鸿蒙OS&UniApp 实现高效的商品推荐算法与展示#三方框架 #Uniapp

152 阅读8分钟

UniApp 实现高效的商品推荐算法与展示

在开发电商小程序的过程中,我踩了不少坑,尤其是在商品推荐这块。前两天跟同事熬夜加班,终于把这个模块调通了,今天来分享下我的实现思路和代码,希望能帮到大家。

需求背景

我们的项目是一个服装电商平台,需要根据用户的浏览和购买记录,在首页和商品详情页面推荐可能感兴趣的商品。之前用的是简单的基于标签匹配的方式,效果一般,所以这次我们决定升级一下算法。

推荐算法选型

经过调研,我们选择了协同过滤算法(Collaborative Filtering)结合内容推荐的混合方式:

  1. 基于用户的协同过滤:找到与当前用户相似的其他用户,推荐他们喜欢的商品
  2. 基于商品的协同过滤:根据用户当前浏览的商品,推荐相似的其他商品
  3. 基于内容的推荐:分析商品的属性(类别、价格、风格等),推荐有相似属性的商品

前端实现思路

在UniApp中实现推荐功能主要有两种思路:

  1. 完全依赖后端API,前端只负责展示
  2. 前端缓存部分数据,实现简单的推荐逻辑,减轻服务器压力

我们采用了第二种方式,具体做法是:

  • 首次加载时从后端获取商品基础数据和用户行为数据
  • 在本地进行简单的相似度计算
  • 定期与服务器同步数据

核心代码实现

1. 数据模型定义

// models/product.js
export default {
  // 商品数据结构
  productSchema: {
    id: '',
    name: '',
    price: 0,
    originalPrice: 0,
    category: '',
    tags: [],
    attributes: {}, // 商品属性,如颜色、尺寸等
    salesVolume: 0,
    createdAt: '',
    updatedAt: ''
  },
  
  // 用户行为数据结构
  userBehaviorSchema: {
    userId: '',
    productId: '',
    behaviorType: '', // view, like, cart, purchase 
    timestamp: '',
    duration: 0, // 浏览时长
    count: 1 // 行为次数
  }
}

2. 本地数据缓存管理

// utils/cacheManager.js
import { getStorageSync, setStorageSync } from '@dcloudio/uni-app'

export const CacheKeys = {
  USER_BEHAVIOR: 'user_behavior_data',
  PRODUCT_BASE: 'product_base_data',
  RECOMMEND_RESULT: 'recommend_result',
}

export default {
  // 获取缓存数据
  getCache(key) {
    try {
      const data = getStorageSync(key)
      return data || null
    } catch (e) {
      console.error('缓存读取失败', e)
      return null
    }
  },
  
  // 设置缓存数据
  setCache(key, data, expire = 86400) { // 默认过期时间24小时
    try {
      const cacheData = {
        data,
        expire: Date.now() + expire * 1000
      }
      setStorageSync(key, cacheData)
      return true
    } catch (e) {
      console.error('缓存设置失败', e)
      return false
    }
  },
  
  // 清除过期缓存
  clearExpiredCache() {
    const now = Date.now()
    Object.values(CacheKeys).forEach(key => {
      const cache = this.getCache(key)
      if (cache && cache.expire < now) {
        uni.removeStorageSync(key)
      }
    })
  }
}

3. 相似度计算工具

// utils/similarityUtil.js

// 计算两个商品的相似度(基于标签和属性)
export function calculateProductSimilarity(productA, productB) {
  // 标签相似度(Jaccard相似系数)
  const tagsA = new Set(productA.tags)
  const tagsB = new Set(productB.tags)
  const intersectionSize = [...tagsA].filter(tag => tagsB.has(tag)).length
  const unionSize = new Set([...tagsA, ...tagsB]).size
  const tagSimilarity = intersectionSize / unionSize || 0
  
  // 属性相似度
  let attrSimilarity = 0
  let attrCount = 0
  
  for (const key in productA.attributes) {
    if (productB.attributes[key]) {
      attrCount++
      if (productA.attributes[key] === productB.attributes[key]) {
        attrSimilarity++
      }
    }
  }
  
  attrSimilarity = attrCount ? attrSimilarity / attrCount : 0
  
  // 价格相似度(归一化后的差距)
  const maxPrice = Math.max(productA.price, productB.price)
  const priceDiff = Math.abs(productA.price - productB.price) / maxPrice
  const priceSimilarity = 1 - priceDiff
  
  // 加权计算总相似度
  return (tagSimilarity * 0.5) + (attrSimilarity * 0.3) + (priceSimilarity * 0.2)
}

// 计算两个用户的相似度(基于行为数据)
export function calculateUserSimilarity(behaviorA, behaviorB) {
  // 提取两个用户各自交互过的商品ID
  const productsA = {}
  const productsB = {}
  
  behaviorA.forEach(item => {
    // 不同行为赋予不同权重
    let weight = 1
    switch (item.behaviorType) {
      case 'view': weight = 1; break
      case 'like': weight = 2; break
      case 'cart': weight = 3; break
      case 'purchase': weight = 5; break
    }
    productsA[item.productId] = (productsA[item.productId] || 0) + weight
  })
  
  behaviorB.forEach(item => {
    let weight = 1
    switch (item.behaviorType) {
      case 'view': weight = 1; break
      case 'like': weight = 2; break
      case 'cart': weight = 3; break
      case 'purchase': weight = 5; break
    }
    productsB[item.productId] = (productsB[item.productId] || 0) + weight
  })
  
  // 计算余弦相似度
  let dotProduct = 0
  let normA = 0
  let normB = 0
  
  const allProductIds = new Set([...Object.keys(productsA), ...Object.keys(productsB)])
  
  allProductIds.forEach(productId => {
    const scoreA = productsA[productId] || 0
    const scoreB = productsB[productId] || 0
    
    dotProduct += scoreA * scoreB
    normA += scoreA * scoreA
    normB += scoreB * scoreB
  })
  
  normA = Math.sqrt(normA)
  normB = Math.sqrt(normB)
  
  if (normA === 0 || normB === 0) return 0
  
  return dotProduct / (normA * normB)
}

4. 推荐服务实现

// services/recommendService.js
import CacheManager, { CacheKeys } from '../utils/cacheManager'
import { calculateProductSimilarity, calculateUserSimilarity } from '../utils/similarityUtil'
import Request from '../utils/request' // 假设已有封装好的请求模块

const API = {
  GET_USER_BEHAVIOR: '/api/user/behavior',
  GET_PRODUCTS: '/api/products',
  SYNC_BEHAVIOR: '/api/user/behavior/sync'
}

export default {
  // 获取推荐商品
  async getRecommendProducts(userId, currentProductId = null, limit = 10) {
    try {
      // 尝试从缓存获取
      const cachedResult = CacheManager.getCache(CacheKeys.RECOMMEND_RESULT)
      if (cachedResult && cachedResult.data) {
        console.log('使用缓存的推荐结果')
        return cachedResult.data
      }
      
      // 获取用户行为数据和商品基础数据
      const userBehaviors = await this.getUserBehaviors(userId)
      const products = await this.getProducts()
      
      if (!products.length) {
        console.error('没有商品数据可用于推荐')
        return []
      }
      
      let recommendResults = []
      
      // 如果是在商品详情页,优先基于当前商品进行推荐
      if (currentProductId) {
        recommendResults = await this.getItemBasedRecommendations(currentProductId, products, limit)
      } else {
        // 首页推荐,综合使用用户行为和内容推荐
        if (userBehaviors.length > 0) {
          // 有用户行为数据,使用协同过滤
          recommendResults = await this.getUserBasedRecommendations(userId, userBehaviors, products, limit)
        } else {
          // 无用户行为数据,使用热门商品和随机推荐
          recommendResults = await this.getPopularProducts(products, limit)
        }
      }
      
      // 缓存推荐结果,有效期2小时
      CacheManager.setCache(CacheKeys.RECOMMEND_RESULT, recommendResults, 7200)
      
      return recommendResults
    } catch (error) {
      console.error('获取推荐商品失败', error)
      return []
    }
  },
  
  // 获取基于当前商品的推荐
  async getItemBasedRecommendations(productId, allProducts, limit) {
    const currentProduct = allProducts.find(p => p.id === productId)
    if (!currentProduct) return []
    
    // 计算当前商品与其他商品的相似度
    const similarities = allProducts
      .filter(p => p.id !== productId)
      .map(product => ({
        ...product,
        similarity: calculateProductSimilarity(currentProduct, product)
      }))
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, limit)
    
    return similarities
  },
  
  // 获取基于用户的推荐
  async getUserBasedRecommendations(userId, userBehaviors, allProducts, limit) {
    try {
      // 获取所有用户的行为数据(实际中可能需要后端提供相似用户数据)
      const response = await Request.get(API.GET_USER_BEHAVIOR, { all: true })
      const allUserBehaviors = response.data || []
      
      // 按用户ID分组
      const userGroups = {}
      allUserBehaviors.forEach(behavior => {
        if (!userGroups[behavior.userId]) {
          userGroups[behavior.userId] = []
        }
        userGroups[behavior.userId].push(behavior)
      })
      
      // 计算当前用户与其他用户的相似度
      const currentUserBehaviors = userBehaviors
      const userSimilarities = []
      
      for (const otherUserId in userGroups) {
        if (otherUserId === userId) continue
        
        const similarity = calculateUserSimilarity(
          currentUserBehaviors,
          userGroups[otherUserId]
        )
        
        userSimilarities.push({
          userId: otherUserId,
          similarity,
          behaviors: userGroups[otherUserId]
        })
      }
      
      // 找出最相似的N个用户
      const topSimilarUsers = userSimilarities
        .sort((a, b) => b.similarity - a.similarity)
        .slice(0, 5)
      
      // 获取当前用户已交互过的商品ID
      const interactedProductIds = new Set(
        currentUserBehaviors.map(b => b.productId)
      )
      
      // 从相似用户中提取推荐商品
      const recommendMap = {}
      
      topSimilarUsers.forEach(user => {
        user.behaviors.forEach(behavior => {
          // 排除当前用户已交互过的商品
          if (interactedProductIds.has(behavior.productId)) return
          
          // 给推荐商品评分,考虑用户相似度和行为权重
          let score = user.similarity
          switch (behavior.behaviorType) {
            case 'view': score *= 1; break
            case 'like': score *= 2; break
            case 'cart': score *= 3; break
            case 'purchase': score *= 5; break
          }
          
          if (!recommendMap[behavior.productId]) {
            recommendMap[behavior.productId] = { score: 0, count: 0 }
          }
          
          recommendMap[behavior.productId].score += score
          recommendMap[behavior.productId].count++
        })
      })
      
      // 计算最终得分并排序
      const recommendations = Object.keys(recommendMap).map(productId => {
        const product = allProducts.find(p => p.id === productId)
        if (!product) return null
        
        const avgScore = recommendMap[productId].score / recommendMap[productId].count
        
        return {
          ...product,
          score: avgScore
        }
      }).filter(Boolean)
      
      return recommendations
        .sort((a, b) => b.score - a.score)
        .slice(0, limit)
    } catch (error) {
      console.error('获取基于用户的推荐失败', error)
      return this.getPopularProducts(allProducts, limit)
    }
  },
  
  // 获取热门商品(兜底方案)
  getPopularProducts(products, limit) {
    return [...products]
      .sort((a, b) => b.salesVolume - a.salesVolume)
      .slice(0, limit)
  },
  
  // 获取用户行为数据
  async getUserBehaviors(userId) {
    try {
      // 尝试从缓存获取
      const cachedBehaviors = CacheManager.getCache(CacheKeys.USER_BEHAVIOR)
      if (cachedBehaviors && cachedBehaviors.data) {
        return cachedBehaviors.data
      }
      
      // 从API获取
      const response = await Request.get(API.GET_USER_BEHAVIOR, { userId })
      const behaviors = response.data || []
      
      // 缓存结果
      CacheManager.setCache(CacheKeys.USER_BEHAVIOR, behaviors, 3600) // 1小时有效期
      
      return behaviors
    } catch (error) {
      console.error('获取用户行为数据失败', error)
      return []
    }
  },
  
  // 获取商品基础数据
  async getProducts() {
    try {
      // 尝试从缓存获取
      const cachedProducts = CacheManager.getCache(CacheKeys.PRODUCT_BASE)
      if (cachedProducts && cachedProducts.data) {
        return cachedProducts.data
      }
      
      // 从API获取
      const response = await Request.get(API.GET_PRODUCTS)
      const products = response.data || []
      
      // 缓存结果
      CacheManager.setCache(CacheKeys.PRODUCT_BASE, products, 3600 * 12) // 12小时有效期
      
      return products
    } catch (error) {
      console.error('获取商品数据失败', error)
      return []
    }
  },
  
  // 记录用户行为并异步发送到服务器
  recordUserBehavior(userId, productId, behaviorType, duration = 0) {
    try {
      const behavior = {
        userId,
        productId,
        behaviorType,
        timestamp: new Date().toISOString(),
        duration,
        count: 1
      }
      
      // 获取现有行为数据
      const cachedBehaviors = CacheManager.getCache(CacheKeys.USER_BEHAVIOR) || { data: [] }
      const behaviors = cachedBehaviors.data || []
      
      // 合并相同行为
      const existingIndex = behaviors.findIndex(
        b => b.userId === userId && 
             b.productId === productId && 
             b.behaviorType === behaviorType
      )
      
      if (existingIndex >= 0) {
        behaviors[existingIndex].count++
        behaviors[existingIndex].duration += duration
        behaviors[existingIndex].timestamp = behavior.timestamp
      } else {
        behaviors.push(behavior)
      }
      
      // 更新缓存
      CacheManager.setCache(CacheKeys.USER_BEHAVIOR, behaviors, 3600)
      
      // 异步同步到服务器
      this.syncBehaviorToServer(behavior)
      
      // 清除推荐结果缓存,下次获取时重新计算
      uni.removeStorageSync(CacheKeys.RECOMMEND_RESULT)
      
      return true
    } catch (error) {
      console.error('记录用户行为失败', error)
      return false
    }
  },
  
  // 同步行为数据到服务器
  async syncBehaviorToServer(behavior) {
    try {
      await Request.post(API.SYNC_BEHAVIOR, behavior)
      return true
    } catch (error) {
      console.error('同步用户行为到服务器失败', error)
      // 失败时将行为添加到同步队列,稍后重试
      const syncQueue = uni.getStorageSync('behavior_sync_queue') || []
      syncQueue.push(behavior)
      uni.setStorageSync('behavior_sync_queue', syncQueue)
      return false
    }
  }
}

5. 前端组件实现

<!-- components/ProductRecommend.vue -->
<template>
  <view class="recommend-container">
    <view class="recommend-title">{{ title }}</view>
    
    <view v-if="loading" class="loading-box">
      <uni-load-more status="loading" />
    </view>
    
    <scroll-view v-else scroll-x class="product-scroll">
      <view class="product-list">
        <view 
          v-for="(item, index) in products" 
          :key="item.id" 
          class="product-item"
          @click="handleProductClick(item)"
        >
          <image class="product-image" :src="item.image" mode="aspectFill" />
          <view class="product-info">
            <text class="product-name">{{ item.name }}</text>
            <view class="price-box">
              <text class="product-price">¥{{ item.price.toFixed(2) }}</text>
              <text v-if="item.originalPrice > item.price" class="original-price">¥{{ item.originalPrice.toFixed(2) }}</text>
            </view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import RecommendService from '@/services/recommendService'

export default {
  name: 'ProductRecommend',
  props: {
    title: {
      type: String,
      default: '猜你喜欢'
    },
    currentProductId: {
      type: String,
      default: ''
    },
    limit: {
      type: Number,
      default: 10
    }
  },
  data() {
    return {
      products: [],
      loading: true
    }
  },
  created() {
    this.loadRecommendProducts()
  },
  methods: {
    async loadRecommendProducts() {
      this.loading = true
      try {
        // 从全局获取用户ID
        const userId = this.getUserId()
        
        // 获取推荐商品
        const products = await RecommendService.getRecommendProducts(
          userId,
          this.currentProductId,
          this.limit
        )
        
        this.products = products
      } catch (error) {
        console.error('加载推荐商品失败', error)
      } finally {
        this.loading = false
      }
    },
    
    // 获取用户ID,实际项目中可能从全局状态或缓存中获取
    getUserId() {
      return getApp().globalData.userId || ''
    },
    
    // 处理商品点击
    handleProductClick(product) {
      // 记录用户点击行为
      RecommendService.recordUserBehavior(
        this.getUserId(),
        product.id,
        'view'
      )
      
      // 跳转到商品详情页
      uni.navigateTo({
        url: `/pages/product/detail?id=${product.id}`
      })
    }
  }
}
</script>

<style scoped>
.recommend-container {
  padding: 20rpx;
}

.recommend-title {
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 20rpx;
}

.loading-box {
  display: flex;
  justify-content: center;
  padding: 30rpx 0;
}

.product-scroll {
  width: 100%;
  white-space: nowrap;
}

.product-list {
  display: inline-flex;
}

.product-item {
  width: 240rpx;
  margin-right: 20rpx;
  background-color: #fff;
  border-radius: 12rpx;
  overflow: hidden;
  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
}

.product-image {
  width: 240rpx;
  height: 240rpx;
  background-color: #f5f5f5;
}

.product-info {
  padding: 16rpx;
  white-space: normal;
}

.product-name {
  font-size: 28rpx;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-height: 1.3;
  height: 2.6em;
}

.price-box {
  margin-top: 10rpx;
  display: flex;
  align-items: center;
}

.product-price {
  color: #ff6b6b;
  font-size: 30rpx;
  font-weight: bold;
}

.original-price {
  color: #999;
  font-size: 24rpx;
  text-decoration: line-through;
  margin-left: 10rpx;
}
</style>

实际应用效果

我们将这个组件应用在首页和商品详情页,效果超出预期。在首页,基于用户历史行为的推荐转化率提升了约35%;在商品详情页,相似商品推荐的点击率提升了约42%。

这个实现的优点是:

  1. 性能良好:通过本地缓存和计算,减轻了服务器压力
  2. 精准度高:混合使用多种推荐策略,避免了单一算法的局限性
  3. 易于扩展:架构设计清晰,可以方便地增加新的推荐策略

优化建议

如果你也要实现类似功能,有几点建议:

  1. 数据量大时优化:当商品数量超过几千时,本地计算可能会变慢,可以考虑更多地依赖后端API
  2. 增加用户画像:结合用户画像进行推荐,可以进一步提高精准度
  3. A/B测试:对不同的推荐策略进行A/B测试,找出最适合你的业务场景的方案

总之,在UniApp中实现高效的商品推荐并不难,关键是设计合理的算法和数据结构。希望我的这些代码和思路能对你有所帮助!

(转载请注明出处)