Penny Lens 储蓄游戏模块详解

2025年1月27日
9 分钟阅读
作者:Penny Lens Team

储蓄游戏模块

🎮 概述

储蓄游戏模块是 Penny Lens 应用的创新功能模块,通过游戏化的方式激励用户控制消费、养成储蓄习惯。基于周消费目标设定,通过实时计算和可视化展示帮助用户达成储蓄目标。

🏗️ 模块架构

核心组件

src/
├── pages/savings/ # 储蓄页面
│ ├── index.vue # 储蓄主页
│ ├── archive.vue # 储蓄历史
│ ├── virtual-transactions.vue # 虚拟交易明细
│ └── components/ # 储蓄页面组件
│ ├── WeeklyProgress.vue # 周进度组件
│ ├── PiggyBankCard.vue # 存钱罐卡片
│ ├── ExpenseDetail.vue # 消费明细
│ └── ActionBar.vue # 操作栏
├── store/savings.ts # 储蓄状态管理
├── services/api/savings.ts # 储蓄API服务
├── utils/savingCalculator.ts # 储蓄计算器
├── types/savings.ts # 储蓄类型定义
└── enums/savingsEnum.ts # 储蓄枚举

🎯 核心概念

储蓄目标系统

// 周目标接口
interface WeeklyGoal {
  id: string;
  userId: string;
  weekStartTimestamp: number;  // 周开始时间戳
  weekEndTimestamp: number;    // 周结束时间戳
  goalCents: number;           // 目标金额(分)
}
 
// 周归档记录
interface WeekArchive {
  weekId: string;              // 周ID (如: 2025-W34)
  userId: string;
  startDateTimestamp: number;
  endDateTimestamp: number;
  goalCents: number;
  actualSpentCents: number;
  savedCents: number;
  status: "done" | "partial" | "failed";
}

虚拟交易系统

// 虚拟交易记录
interface VirtualTransaction {
  id: string;
  userId: string;
  weekId: string;
  type: "deposit" | "withdraw";
  amountCents: number;
  description: string;
  createdAt: number;
}
 
// 虚拟余额
interface VirtualSaving {
  userId: string;
  balanceCents: number;
  locked?: boolean;
}

🔧 核心功能

1. 储蓄概览

功能描述: 显示当前用户的储蓄概览信息

实现流程:

  1. 获取当前周的时间范围
  2. 查询用户当前周的储蓄目标
  3. 从记账记录聚合计算实际消费金额
  4. 计算节省金额(目标 - 实际消费)
  5. 获取虚拟余额总额
  6. 计算宠物等级和心情状态

代码示例:

// 储蓄概览响应
interface SavingsOverviewResponse {
  weekGoal: WeeklyGoal | null;
  actualSpentCents: number;
  savedCents: number;
  virtualBalanceCents: number;
  weekStartTimestamp: number;
  weekEndTimestamp: number;
  weekId: string;
  petLevel: number;
  petMood: string;
  progressPercentage: number;
}
 
// 获取储蓄概览
async function getSavingsOverview(): Promise<SavingsOverviewResponse> {
  const response = await savingsApi.getOverview()
  return response.data
}
 
// 计算宠物等级
function calculatePetLevel(totalSavedCents: number): number {
  const LEVEL_THRESHOLDS = {
    LEVEL_2: 5000,   // 50元
    LEVEL_3: 20000,  // 200元
    LEVEL_4: 50000,  // 500元
    LEVEL_5: 100000  // 1000元
  }
  
  if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_2) return 1
  if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_3) return 2
  if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_4) return 3
  if (totalSavedCents < LEVEL_THRESHOLDS.LEVEL_5) return 4
  return 5
}
 
// 计算宠物心情
function calculatePetMood(progressPercentage: number): string {
  if (progressPercentage >= 100) return 'excited'
  if (progressPercentage >= 80) return 'happy'
  if (progressPercentage >= 60) return 'normal'
  if (progressPercentage >= 40) return 'worried'
  return 'sad'
}

2. 设置储蓄目标

功能描述: 设置或更新当前周的储蓄目标

实现流程:

  1. 用户输入目标金额
  2. 验证目标金额有效性
  3. 保存到后端
  4. 更新本地状态

代码示例:

// 设置储蓄目标
async function setWeeklyGoal(goalData: WeeklyGoalRequest): Promise<WeeklyGoal> {
  // 参数验证
  if (goalData.goalCents <= 0) {
    throw new Error('目标金额必须大于0')
  }
  
  if (goalData.weekStartTimestamp >= goalData.weekEndTimestamp) {
    throw new Error('周开始时间必须早于结束时间')
  }
  
  const response = await savingsApi.setGoal(goalData)
  return response.data
}
 
// 获取当前周范围
function getCurrentWeekRange(): { start: Date; end: Date } {
  const now = new Date()
  const startDate = new Date(now)
  const dayOfWeek = now.getDay()
  
  // 调整为周一为周开始
  const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek
  startDate.setDate(now.getDate() + diff)
  startDate.setHours(0, 0, 0, 0)
  
  const endDate = new Date(startDate)
  endDate.setDate(startDate.getDate() + 6)
  endDate.setHours(23, 59, 59, 999)
  
  return { start: startDate, end: endDate }
}

3. 虚拟交易系统

功能描述: 管理虚拟余额的存取操作

实现流程:

  1. 用户选择交易类型(存入/提取)
  2. 输入交易金额和原因
  3. 验证余额是否充足
  4. 创建虚拟交易记录
  5. 更新虚拟余额

代码示例:

// 虚拟余额提取
async function withdrawFromVirtualBalance(amountCents: number, reason: string): Promise<WithdrawResponse> {
  // 检查当前虚拟余额
  const overview = await getSavingsOverview()
  
  if (overview.virtualBalanceCents < amountCents) {
    return {
      success: false,
      newBalanceCents: overview.virtualBalanceCents,
      message: '虚拟余额不足'
    }
  }
  
  const response = await savingsApi.withdraw({
    amountCents,
    reason
  })
  
  return response.data
}
 
// 虚拟余额存入(自动)
async function depositToVirtualBalance(weekId: string, savedCents: number): Promise<void> {
  if (savedCents > 0) {
    await savingsApi.deposit({
      weekId,
      amountCents: savedCents,
      description: `第${weekId}周储蓄奖励`
    })
  }
}

4. 周归档功能

功能描述: 归档指定周的储蓄记录

实现流程:

  1. 检查该周是否已归档
  2. 获取周目标和实际消费
  3. 计算节省金额和达成状态
  4. 如有节省金额,自动添加到虚拟余额
  5. 创建归档记录

代码示例:

// 归档周记录
async function archiveWeek(weekStartTimestamp: number, weekEndTimestamp: number): Promise<WeekArchive> {
  const response = await savingsApi.archiveWeek({
    weekStartTimestamp,
    weekEndTimestamp
  })
  
  return response.data
}
 
// 计算达成状态
function calculateAchievementStatus(goalCents: number, actualSpentCents: number): string {
  if (actualSpentCents <= goalCents) return "done"
  if (actualSpentCents <= goalCents * 1.2) return "partial" // 超支20%内
  return "failed"
}

🎮 游戏化设计

1. 存钱罐等级系统

功能描述: 基于虚拟余额的等级成长系统

代码示例:

// 宠物等级配置
const PET_LEVELS = [
  {
    level: 1,
    name: '小萌新',
    description: '刚开始存钱的小萌新',
    requiredSavings: 0,
    emoji: '🐣',
    color: '#ffd700'
  },
  {
    level: 2,
    name: '存钱新手',
    description: '已经存了50元的新手',
    requiredSavings: 5000,
    emoji: '🐥',
    color: '#ffb347'
  },
  {
    level: 3,
    name: '存钱达人',
    description: '存了200元的达人',
    requiredSavings: 20000,
    emoji: '🐔',
    color: '#ff8c00'
  },
  {
    level: 4,
    name: '存钱专家',
    description: '存了500元的专家',
    requiredSavings: 50000,
    emoji: '🦅',
    color: '#ff6347'
  },
  {
    level: 5,
    name: '存钱大师',
    description: '存了1000元的大师',
    requiredSavings: 100000,
    emoji: '🦅',
    color: '#ff4500'
  }
]
 
// 获取宠物等级信息
function getPetLevelInfo(level: number): PetLevel {
  return PET_LEVELS.find(pet => pet.level === level) || PET_LEVELS[0]
}
 
// 计算下一等级进度
function calculateNextLevelProgress(currentLevel: number, totalSaved: number): {
  nextLevel: PetLevel;
  progress: number;
  remaining: number;
} {
  const nextLevel = PET_LEVELS.find(pet => pet.level === currentLevel + 1)
  if (!nextLevel) {
    return {
      nextLevel: PET_LEVELS[PET_LEVELS.length - 1],
      progress: 100,
      remaining: 0
    }
  }
  
  const currentLevelInfo = getPetLevelInfo(currentLevel)
  const progress = Math.min(100, ((totalSaved - currentLevelInfo.requiredSavings) / (nextLevel.requiredSavings - currentLevelInfo.requiredSavings)) * 100)
  const remaining = Math.max(0, nextLevel.requiredSavings - totalSaved)
  
  return {
    nextLevel,
    progress,
    remaining
  }
}

2. 节省力评分系统

功能描述: 基于实际消费行为的综合评分体系

代码示例:

// 节省力统计
interface SavingPowerStats {
  savedDays: number;           // 节省天数
  totalSavingAmount: number;   // 总节省金额
  maxDailySaving: number;      // 最大单日节省
  savingStreak: number;        // 节省连续天数
  savingPower: number;          // 节省力评分 0-100
  savingRate: number;          // 节省率 0-100%
  totalOverspent: number;      // 总超支金额
  healthStatus: 'healthy' | 'warning' | 'danger' | 'critical';
  overBudgetRatio: number;     // 超支比例
}
 
// 计算节省力评分
function calculateSavingPower(
  recordedDays: CalculatedDayData[],
  weekGoal: number
): SavingPowerStats {
  const savedDays = recordedDays.filter(day => day.dailySaving > 0).length
  const totalSavingAmount = recordedDays.reduce((sum, day) => sum + day.dailySaving, 0)
  const maxDailySaving = Math.max(...recordedDays.map(day => day.dailySaving), 0)
  const savingStreak = calculateSavingStreak(recordedDays)
  const totalOverspent = recordedDays.reduce((sum, day) => sum + Math.max(0, day.dailySpent - day.dailyBudget), 0)
  
  // 计算节省力评分 (0-100分)
  const savedDaysRatio = recordedDays.length > 0 ? (savedDays / recordedDays.length) * 40 : 0
  const averageSavingRate = recordedDays.length > 0 ? (totalSavingAmount / (recordedDays.length * weekGoal / 7)) * 30 : 0
  const maxSavingBonus = Math.min(15, (maxDailySaving / (weekGoal / 7)) * 15)
  const streakBonus = Math.min(15, (savingStreak / 7) * 15)
  
  const savingPower = Math.min(100, savedDaysRatio + averageSavingRate + maxSavingBonus + streakBonus)
  const savingRate = weekGoal > 0 ? Math.min(100, (totalSavingAmount / weekGoal) * 100) : 0
  const overBudgetRatio = weekGoal > 0 ? (totalOverspent / weekGoal) * 100 : 0
  
  // 健康状态
  let healthStatus: 'healthy' | 'warning' | 'danger' | 'critical'
  if (savingPower >= 80) healthStatus = 'healthy'
  else if (savingPower >= 60) healthStatus = 'warning'
  else if (savingPower >= 40) healthStatus = 'danger'
  else healthStatus = 'critical'
  
  return {
    savedDays,
    totalSavingAmount,
    maxDailySaving,
    savingStreak,
    savingPower: Math.round(savingPower * 10) / 10,
    savingRate: Math.round(savingRate * 10) / 10,
    totalOverspent,
    healthStatus,
    overBudgetRatio: Math.round(overBudgetRatio * 10) / 10
  }
}
 
// 计算节省连续天数
function calculateSavingStreak(recordedDays: CalculatedDayData[]): number {
  let streak = 0
  let maxStreak = 0
  
  for (let i = recordedDays.length - 1; i >= 0; i--) {
    if (recordedDays[i].dailySaving > 0) {
      streak++
      maxStreak = Math.max(maxStreak, streak)
    } else {
      streak = 0
    }
  }
  
  return maxStreak
}

3. 成就系统

功能描述: 基于储蓄行为的成就奖励系统

代码示例:

// 成就类型
interface Achievement {
  id: string;
  name: string;
  description: string;
  icon: string;
  condition: AchievementCondition;
  reward: AchievementReward;
  unlocked: boolean;
  unlockedAt?: number;
}
 
// 成就条件
interface AchievementCondition {
  type: 'saving_streak' | 'total_saved' | 'weekly_goal' | 'pet_level';
  value: number;
  period?: 'daily' | 'weekly' | 'monthly' | 'all_time';
}
 
// 成就奖励
interface AchievementReward {
  type: 'virtual_balance' | 'pet_exp' | 'badge';
  amount: number;
  description: string;
}
 
// 成就配置
const ACHIEVEMENTS: Achievement[] = [
  {
    id: 'first_save',
    name: '初次储蓄',
    description: '完成第一次储蓄',
    icon: 'i-mdi-trophy',
    condition: { type: 'total_saved', value: 1 },
    reward: { type: 'virtual_balance', amount: 1000, description: '获得10元虚拟余额' },
    unlocked: false
  },
  {
    id: 'saving_streak_7',
    name: '连续储蓄达人',
    description: '连续7天完成储蓄目标',
    icon: 'i-mdi-fire',
    condition: { type: 'saving_streak', value: 7 },
    reward: { type: 'virtual_balance', amount: 5000, description: '获得50元虚拟余额' },
    unlocked: false
  },
  {
    id: 'pet_level_5',
    name: '存钱大师',
    description: '宠物等级达到5级',
    icon: 'i-mdi-crown',
    condition: { type: 'pet_level', value: 5 },
    reward: { type: 'badge', amount: 1, description: '获得大师徽章' },
    unlocked: false
  }
]
 
// 检查成就解锁
function checkAchievements(userStats: UserStats): Achievement[] {
  const unlockedAchievements: Achievement[] = []
  
  ACHIEVEMENTS.forEach(achievement => {
    if (achievement.unlocked) return
    
    let conditionMet = false
    
    switch (achievement.condition.type) {
      case 'saving_streak':
        conditionMet = userStats.savingStreak >= achievement.condition.value
        break
      case 'total_saved':
        conditionMet = userStats.totalSaved >= achievement.condition.value
        break
      case 'weekly_goal':
        conditionMet = userStats.weeklyGoalsMet >= achievement.condition.value
        break
      case 'pet_level':
        conditionMet = userStats.petLevel >= achievement.condition.value
        break
    }
    
    if (conditionMet) {
      achievement.unlocked = true
      achievement.unlockedAt = Date.now()
      unlockedAchievements.push(achievement)
    }
  })
  
  return unlockedAchievements
}

📊 储蓄计算器

功能描述: 核心的储蓄计算逻辑

代码示例:

// 储蓄计算器
export class SavingCalculator {
  // 计算周预算
  static calculateWeeklyBudget(
    weekGoalYuan: number,
    dailyExpenseData: Array<{ date: string, amount: number }>,
    currentWeekStart: Date
  ): WeeklyCalculationResult {
    const weekDays = this.generateWeekDays(currentWeekStart)
    const dailyBudget = weekGoalYuan / 7
    
    // 计算每日数据
    const calculatedDays = weekDays.map(day => {
      const dayData = dailyExpenseData.find(d => d.date === this.formatDate(day.date))
      const dailySpent = dayData ? dayData.amount : 0
      const dailySaving = Math.max(0, dailyBudget - dailySpent)
      
      return {
        date: day.date,
        isToday: day.isToday,
        hasRecord: !!dayData,
        dailySpent,
        dailyBudget,
        dailySaving,
        status: this.calculateDailyStatus(dailySpent, dailyBudget)
      } as CalculatedDayData
    })
    
    // 计算总节省金额
    const actualSavedYuan = calculatedDays.reduce((sum, day) => sum + day.dailySaving, 0)
    
    // 计算节省力统计
    const savingPowerStats = this.calculateSavingPowerStats(calculatedDays, weekGoalYuan)
    
    return {
      weekDays: calculatedDays,
      actualSavedYuan,
      savingPowerStats
    }
  }
  
  // 生成一周的天数
  private static generateWeekDays(weekStart: Date): Array<{ date: Date; isToday: boolean }> {
    const days = []
    const today = new Date()
    
    for (let i = 0; i < 7; i++) {
      const date = new Date(weekStart)
      date.setDate(weekStart.getDate() + i)
      
      days.push({
        date,
        isToday: this.isSameDay(date, today)
      })
    }
    
    return days
  }
  
  // 计算每日状态
  private static calculateDailyStatus(dailySpent: number, dailyBudget: number): DailyStatusType {
    if (dailySpent === 0) return DAILY_STATUS.NORMAL
    
    const ratio = dailySpent / dailyBudget
    
    if (ratio <= 0.8) return DAILY_STATUS.SAVED
    if (ratio <= 1.0) return DAILY_STATUS.NORMAL
    if (ratio <= 1.2) return DAILY_STATUS.OVERSPENT
    return DAILY_STATUS.EXTREME_OVERSPENT
  }
  
  // 格式化日期
  private static formatDate(date: Date): string {
    return date.toISOString().split('T')[0]
  }
  
  // 判断是否为同一天
  private static isSameDay(date1: Date, date2: Date): boolean {
    return date1.toDateString() === date2.toDateString()
  }
}

🔄 状态管理

Pinia Store

功能描述: 使用Pinia管理储蓄游戏状态

代码示例:

// 储蓄状态管理
export const useSavingsStore = defineStore('savings', () => {
  // 状态
  const weekGoalYuan = ref(0)
  const actualSpentYuan = ref(0)
  const savedYuan = ref(0)
  const virtualBalanceYuan = ref(0)
  const weeklyCalculationResult = ref<WeeklyCalculationResult | null>(null)
  const petLevel = ref(1)
  const petMood = ref('normal')
  const isLoading = ref(false)
  
  // 计算属性
  const progressPercentage = computed(() => {
    if (weekGoalYuan.value === 0) return 0
    return Math.min(100, (savedYuan.value / weekGoalYuan.value) * 100)
  })
  
  const isGoalMet = computed(() => actualSpentYuan.value <= weekGoalYuan.value)
  
  const savingPowerStats = computed(() => 
    weeklyCalculationResult.value?.savingPowerStats || {
      savedDays: 0,
      totalSavingAmount: 0,
      maxDailySaving: 0,
      savingStreak: 0,
      savingPower: 0,
      savingRate: 0,
      totalOverspent: 0,
      healthStatus: 'healthy',
      overBudgetRatio: 0
    }
  )
  
  // 操作
  const fetchSavingsOverview = async (): Promise<void> => {
    isLoading.value = true
    try {
      const overview = await savingsService.getOverview()
      
      weekGoalYuan.value = overview.weekGoal ? overview.weekGoal.goalCents / 100 : 0
      actualSpentYuan.value = overview.actualSpentCents / 100
      savedYuan.value = overview.savedCents / 100
      virtualBalanceYuan.value = overview.virtualBalanceCents / 100
      petLevel.value = overview.petLevel
      petMood.value = overview.petMood
      
      // 计算周数据
      if (weekGoalYuan.value > 0) {
        const weekRange = getCurrentWeekRange()
        const expenseData = await getWeeklyExpenseData(weekRange.start, weekRange.end)
        weeklyCalculationResult.value = SavingCalculator.calculateWeeklyBudget(
          weekGoalYuan.value,
          expenseData,
          weekRange.start
        )
      }
    } catch (error) {
      console.error('获取储蓄概览失败:', error)
      throw error
    } finally {
      isLoading.value = false
    }
  }
  
  const setWeeklyGoal = async (goalYuan: number): Promise<void> => {
    try {
      const weekRange = getCurrentWeekRange()
      await savingsService.setGoal({
        weekStartTimestamp: weekRange.start.getTime(),
        weekEndTimestamp: weekRange.end.getTime(),
        goalCents: goalYuan * 100
      })
      
      weekGoalYuan.value = goalYuan
      
      // 重新计算周数据
      const expenseData = await getWeeklyExpenseData(weekRange.start, weekRange.end)
      weeklyCalculationResult.value = SavingCalculator.calculateWeeklyBudget(
        goalYuan,
        expenseData,
        weekRange.start
      )
    } catch (error) {
      console.error('设置周目标失败:', error)
      throw error
    }
  }
  
  const withdrawFromVirtualBalance = async (amountYuan: number, reason: string): Promise<boolean> => {
    try {
      const result = await savingsService.withdraw({
        amountCents: amountYuan * 100,
        reason
      })
      
      if (result.success) {
        virtualBalanceYuan.value = result.newBalanceCents / 100
        return true
      }
      return false
    } catch (error) {
      console.error('提取虚拟余额失败:', error)
      throw error
    }
  }
  
  return {
    weekGoalYuan,
    actualSpentYuan,
    savedYuan,
    virtualBalanceYuan,
    weeklyCalculationResult,
    petLevel,
    petMood,
    isLoading,
    progressPercentage,
    isGoalMet,
    savingPowerStats,
    fetchSavingsOverview,
    setWeeklyGoal,
    withdrawFromVirtualBalance
  }
})

📚 最佳实践

1. 游戏化设计

  • 清晰的进度反馈
  • 有意义的奖励机制
  • 适度的挑战难度
  • 社交分享功能

2. 数据一致性

  • 实时更新储蓄状态
  • 同步虚拟交易记录
  • 处理并发操作
  • 数据验证和错误处理

3. 性能优化

  • 缓存计算结果
  • 懒加载历史数据
  • 优化动画效果
  • 减少不必要的计算

4. 用户体验

  • 直观的视觉反馈
  • 流畅的交互动画
  • 清晰的成就提示
  • 便捷的操作流程

Penny Lens 储蓄游戏模块 - 让储蓄变得更有趣、更有动力 🎮