Penny Lens 储蓄游戏模块详细文档

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

Penny Lens 储蓄游戏模块详细文档

🎯 模块概述

储蓄游戏模块是 Penny Lens 系统的创新功能,通过游戏化设计让储蓄变得有趣和激励。该模块包含虚拟交易、目标管理、成就系统、排行榜等核心功能,帮助用户建立良好的储蓄习惯。

🏗️ 技术架构

核心组件

src/
├── pages/savings/ # 储蓄游戏页面
│ ├── index.vue # 游戏主页
│ ├── goals.vue # 目标管理
│ ├── trading.vue # 虚拟交易
│ ├── achievements.vue # 成就系统
│ ├── leaderboard.vue # 排行榜
│ └── history.vue # 历史记录
├── components/SavingsGame/ # 储蓄游戏组件
│ ├── GoalCard.vue # 目标卡片
│ ├── TradingPanel.vue # 交易面板
│ ├── AchievementBadge.vue # 成就徽章
│ ├── ProgressBar.vue # 进度条
│ └── VirtualWallet.vue # 虚拟钱包
├── store/savingsGame.ts # 储蓄游戏状态管理
├── services/savingsGame.ts # 储蓄游戏服务
├── utils/savingsGame.ts # 储蓄游戏工具
├── types/savingsGame.ts # 储蓄游戏类型定义
└── constants/savingsGame.ts # 储蓄游戏常量

🎮 核心功能

1. 储蓄目标管理

功能描述: 设定和管理储蓄目标

数据结构:

interface SavingsGoal {
  id: string;
  userId: string;
  title: string;              // 目标标题
  description?: string;       // 目标描述
  targetAmount: number;       // 目标金额(分)
  currentAmount: number;      // 当前金额(分)
  startDate: number;          // 开始日期
  endDate: number;            // 结束日期
  status: GoalStatus;         // 目标状态
  category: GoalCategory;     // 目标分类
  priority: GoalPriority;     // 优先级
  isPublic: boolean;          // 是否公开
  tags?: string[];            // 标签
  createdAt: number;
  updatedAt: number;
}
 
enum GoalStatus {
  ACTIVE = 'active',          // 进行中
  COMPLETED = 'completed',     // 已完成
  PAUSED = 'paused',          // 已暂停
  CANCELLED = 'cancelled'     // 已取消
}
 
enum GoalCategory {
  EMERGENCY = 'emergency',    // 应急基金
  VACATION = 'vacation',      // 旅行基金
  EDUCATION = 'education',    // 教育基金
  INVESTMENT = 'investment',   // 投资基金
  PURCHASE = 'purchase',      // 购买基金
  OTHER = 'other'             // 其他
}

实现示例:

// 创建储蓄目标
async function createSavingsGoal(data: CreateSavingsGoalRequest): Promise<SavingsGoal> {
  // 验证数据
  const validation = validateSavingsGoal(data);
  if (!validation.isValid) {
    throw new Error(validation.errors.join(', '));
  }
 
  // 创建目标
  const goal = await savingsGameService.createGoal({
    ...data,
    targetAmount: data.targetAmount * 100, // 转换为分
    currentAmount: 0,
    status: GoalStatus.ACTIVE,
    createdAt: Date.now(),
    updatedAt: Date.now()
  });
 
  return goal;
}
 
// 更新目标进度
async function updateGoalProgress(goalId: string, amount: number): Promise<SavingsGoal> {
  const goal = await savingsGameService.getGoal(goalId);
  if (!goal) {
    throw new Error('目标不存在');
  }
 
  const newAmount = goal.currentAmount + amount;
  const isCompleted = newAmount >= goal.targetAmount;
 
  const updatedGoal = await savingsGameService.updateGoal(goalId, {
    currentAmount: newAmount,
    status: isCompleted ? GoalStatus.COMPLETED : goal.status,
    updatedAt: Date.now()
  });
 
  // 检查是否完成目标
  if (isCompleted && goal.status !== GoalStatus.COMPLETED) {
    await triggerGoalCompletion(goalId);
  }
 
  return updatedGoal;
}

2. 虚拟交易系统

功能描述: 模拟股票交易,增加储蓄的趣味性

数据结构:

interface VirtualStock {
  id: string;
  symbol: string;            // 股票代码
  name: string;               // 股票名称
  currentPrice: number;       // 当前价格(分)
  changePercent: number;      // 涨跌幅
  volume: number;             // 成交量
  marketCap: number;          // 市值
  sector: string;             // 行业
  lastUpdated: number;        // 最后更新时间
}
 
interface VirtualTransaction {
  id: string;
  userId: string;
  stockId: string;
  type: TransactionType;       // 交易类型
  quantity: number;           // 数量
  price: number;              // 价格(分)
  totalAmount: number;        // 总金额(分)
  fee: number;                // 手续费(分)
  date: number;               // 交易日期
  status: TransactionStatus;  // 交易状态
  createdAt: number;
}
 
enum TransactionType {
  BUY = 'buy',               // 买入
  SELL = 'sell'              // 卖出
}
 
enum TransactionStatus {
  PENDING = 'pending',        // 待处理
  COMPLETED = 'completed',    // 已完成
  CANCELLED = 'cancelled'     // 已取消
}

实现示例:

// 买入股票
async function buyStock(data: BuyStockRequest): Promise<VirtualTransaction> {
  const { stockId, quantity, userId } = data;
  
  // 获取股票信息
  const stock = await stockService.getStock(stockId);
  if (!stock) {
    throw new Error('股票不存在');
  }
 
  // 计算交易金额
  const totalAmount = stock.currentPrice * quantity;
  const fee = calculateTradingFee(totalAmount);
  const totalCost = totalAmount + fee;
 
  // 检查虚拟钱包余额
  const wallet = await getVirtualWallet(userId);
  if (wallet.balance < totalCost) {
    throw new Error('虚拟钱包余额不足');
  }
 
  // 创建交易记录
  const transaction = await savingsGameService.createTransaction({
    userId,
    stockId,
    type: TransactionType.BUY,
    quantity,
    price: stock.currentPrice,
    totalAmount,
    fee,
    date: Date.now(),
    status: TransactionStatus.PENDING,
    createdAt: Date.now()
  });
 
  // 执行交易
  await executeTransaction(transaction);
 
  return transaction;
}
 
// 卖出股票
async function sellStock(data: SellStockRequest): Promise<VirtualTransaction> {
  const { stockId, quantity, userId } = data;
  
  // 检查持仓
  const position = await getStockPosition(userId, stockId);
  if (!position || position.quantity < quantity) {
    throw new Error('持仓不足');
  }
 
  // 获取股票信息
  const stock = await stockService.getStock(stockId);
  if (!stock) {
    throw new Error('股票不存在');
  }
 
  // 计算交易金额
  const totalAmount = stock.currentPrice * quantity;
  const fee = calculateTradingFee(totalAmount);
  const netAmount = totalAmount - fee;
 
  // 创建交易记录
  const transaction = await savingsGameService.createTransaction({
    userId,
    stockId,
    type: TransactionType.SELL,
    quantity,
    price: stock.currentPrice,
    totalAmount,
    fee,
    date: Date.now(),
    status: TransactionStatus.PENDING,
    createdAt: Date.now()
  });
 
  // 执行交易
  await executeTransaction(transaction);
 
  return transaction;
}
 
// 计算交易手续费
function calculateTradingFee(amount: number): number {
  // 手续费率为0.1%,最低1分
  const fee = Math.floor(amount * 0.001);
  return Math.max(fee, 1);
}

3. 成就系统

功能描述: 通过成就系统激励用户持续储蓄

数据结构:

interface Achievement {
  id: string;
  title: string;              // 成就标题
  description: string;        // 成就描述
  icon: string;               // 成就图标
  category: AchievementCategory; // 成就分类
  condition: AchievementCondition; // 达成条件
  reward: AchievementReward;  // 奖励
  rarity: AchievementRarity;  // 稀有度
  isHidden: boolean;          // 是否隐藏
  createdAt: number;
}
 
interface UserAchievement {
  id: string;
  userId: string;
  achievementId: string;
  progress: number;           // 当前进度
  isCompleted: boolean;       // 是否完成
  completedAt?: number;       // 完成时间
  claimedAt?: number;         // 领取时间
  createdAt: number;
}
 
enum AchievementCategory {
  SAVINGS = 'savings',        // 储蓄类
  TRADING = 'trading',        // 交易类
  GOALS = 'goals',            // 目标类
  SOCIAL = 'social',          // 社交类
  SPECIAL = 'special'         // 特殊类
}
 
enum AchievementRarity {
  COMMON = 'common',          // 普通
  RARE = 'rare',              // 稀有
  EPIC = 'epic',              // 史诗
  LEGENDARY = 'legendary'     // 传说
}

实现示例:

// 检查成就进度
async function checkAchievementProgress(userId: string): Promise<void> {
  const achievements = await achievementService.getActiveAchievements();
  
  for (const achievement of achievements) {
    const userAchievement = await getUserAchievement(userId, achievement.id);
    
    if (userAchievement && userAchievement.isCompleted) {
      continue; // 已完成的成就跳过
    }
    
    const progress = await calculateAchievementProgress(userId, achievement);
    
    if (progress >= achievement.condition.target) {
      // 完成成就
      await completeAchievement(userId, achievement.id);
    } else {
      // 更新进度
      await updateAchievementProgress(userId, achievement.id, progress);
    }
  }
}
 
// 计算成就进度
async function calculateAchievementProgress(userId: string, achievement: Achievement): Promise<number> {
  switch (achievement.condition.type) {
    case 'total_savings':
      const totalSavings = await getTotalSavings(userId);
      return Math.min(totalSavings, achievement.condition.target);
      
    case 'goal_completed':
      const completedGoals = await getCompletedGoalsCount(userId);
      return Math.min(completedGoals, achievement.condition.target);
      
    case 'trading_volume':
      const tradingVolume = await getTradingVolume(userId);
      return Math.min(tradingVolume, achievement.condition.target);
      
    case 'consecutive_days':
      const consecutiveDays = await getConsecutiveSavingsDays(userId);
      return Math.min(consecutiveDays, achievement.condition.target);
      
    default:
      return 0;
  }
}
 
// 完成成就
async function completeAchievement(userId: string, achievementId: string): Promise<void> {
  const achievement = await achievementService.getAchievement(achievementId);
  if (!achievement) {
    throw new Error('成就不存在');
  }
 
  // 更新用户成就
  await updateUserAchievement(userId, achievementId, {
    isCompleted: true,
    completedAt: Date.now()
  });
 
  // 发放奖励
  await grantAchievementReward(userId, achievement.reward);
 
  // 发送通知
  await sendAchievementNotification(userId, achievement);
}

4. 排行榜系统

功能描述: 通过排行榜增加竞争性和社交性

数据结构:

interface LeaderboardEntry {
  rank: number;               // 排名
  userId: string;              // 用户ID
  username: string;            // 用户名
  avatar?: string;             // 头像
  score: number;               // 分数
  change: number;              // 排名变化
  badges: string[];            // 徽章
}
 
interface Leaderboard {
  type: LeaderboardType;      // 排行榜类型
  period: LeaderboardPeriod;  // 时间周期
  entries: LeaderboardEntry[]; // 排行榜条目
  totalCount: number;          // 总人数
  lastUpdated: number;         // 最后更新时间
}
 
enum LeaderboardType {
  TOTAL_SAVINGS = 'total_savings',    // 总储蓄
  GOAL_COMPLETION = 'goal_completion', // 目标完成
  TRADING_PROFIT = 'trading_profit',  // 交易收益
  ACHIEVEMENTS = 'achievements'       // 成就数量
}
 
enum LeaderboardPeriod {
  DAILY = 'daily',            // 日榜
  WEEKLY = 'weekly',          // 周榜
  MONTHLY = 'monthly',        // 月榜
  ALL_TIME = 'all_time'       // 总榜
}

实现示例:

// 获取排行榜
async function getLeaderboard(type: LeaderboardType, period: LeaderboardPeriod): Promise<Leaderboard> {
  const entries = await calculateLeaderboardEntries(type, period);
  
  return {
    type,
    period,
    entries,
    totalCount: entries.length,
    lastUpdated: Date.now()
  };
}
 
// 计算排行榜条目
async function calculateLeaderboardEntries(type: LeaderboardType, period: LeaderboardPeriod): Promise<LeaderboardEntry[]> {
  const startDate = getPeriodStartDate(period);
  const endDate = getPeriodEndDate(period);
  
  let entries: LeaderboardEntry[] = [];
  
  switch (type) {
    case LeaderboardType.TOTAL_SAVINGS:
      entries = await getTotalSavingsLeaderboard(startDate, endDate);
      break;
      
    case LeaderboardType.GOAL_COMPLETION:
      entries = await getGoalCompletionLeaderboard(startDate, endDate);
      break;
      
    case LeaderboardType.TRADING_PROFIT:
      entries = await getTradingProfitLeaderboard(startDate, endDate);
      break;
      
    case LeaderboardType.ACHIEVEMENTS:
      entries = await getAchievementsLeaderboard(startDate, endDate);
      break;
  }
  
  // 添加排名和变化
  return entries.map((entry, index) => ({
    ...entry,
    rank: index + 1,
    change: await calculateRankChange(entry.userId, type, period)
  }));
}
 
// 获取总储蓄排行榜
async function getTotalSavingsLeaderboard(startDate: number, endDate: number): Promise<LeaderboardEntry[]> {
  const users = await userService.getActiveUsers();
  
  const entries = await Promise.all(
    users.map(async (user) => {
      const totalSavings = await getTotalSavings(user.id, startDate, endDate);
      const achievements = await getUserAchievements(user.id);
      
      return {
        userId: user.id,
        username: user.username,
        avatar: user.avatar,
        score: totalSavings,
        badges: achievements.map(a => a.achievementId),
        change: 0 // 将在后续计算
      };
    })
  );
  
  // 按分数排序
  return entries
    .filter(entry => entry.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, 100); // 取前100名
}

🎨 游戏化设计

激励机制

功能描述: 通过多种激励机制提升用户参与度

实现示例:

// 每日签到奖励
async function dailyCheckIn(userId: string): Promise<CheckInReward> {
  const today = new Date().toDateString();
  const lastCheckIn = await getLastCheckIn(userId);
  
  if (lastCheckIn && lastCheckIn.date === today) {
    throw new Error('今日已签到');
  }
  
  // 计算连续签到天数
  const consecutiveDays = await getConsecutiveCheckInDays(userId);
  const reward = calculateCheckInReward(consecutiveDays);
  
  // 发放奖励
  await grantReward(userId, reward);
  
  // 记录签到
  await recordCheckIn(userId, today, consecutiveDays + 1);
  
  return {
    reward,
    consecutiveDays: consecutiveDays + 1,
    nextReward: calculateCheckInReward(consecutiveDays + 1)
  };
}
 
// 计算签到奖励
function calculateCheckInReward(consecutiveDays: number): Reward {
  const baseReward = 100; // 基础奖励100分
  const bonusMultiplier = Math.min(Math.floor(consecutiveDays / 7), 5); // 每周额外奖励,最多5倍
  
  return {
    type: 'virtual_currency',
    amount: baseReward * (1 + bonusMultiplier * 0.2),
    description: `连续签到${consecutiveDays}天奖励`
  };
}

进度可视化

功能描述: 通过可视化进度条和图表展示储蓄进度

实现示例:

// 目标进度组件
<template>
  <view class="goal-progress">
    <view class="goal-header">
      <text class="goal-title">{{ goal.title }}</text>
      <text class="goal-amount">{{ formatAmount(goal.currentAmount) }} / {{ formatAmount(goal.targetAmount) }}</text>
    </view>
    
    <view class="progress-container">
      <view class="progress-bar">
        <view 
          class="progress-fill" 
          :style="{ width: progressPercentage + '%' }"
        ></view>
      </view>
      <text class="progress-text">{{ progressPercentage.toFixed(1) }}%</text>
    </view>
    
    <view class="goal-stats">
      <view class="stat-item">
        <text class="stat-label">剩余金额</text>
        <text class="stat-value">{{ formatAmount(remainingAmount) }}</text>
      </view>
      <view class="stat-item">
        <text class="stat-label">预计完成</text>
        <text class="stat-value">{{ estimatedCompletionDate }}</text>
      </view>
    </view>
  </view>
</template>
 
<script setup lang="ts">
interface Props {
  goal: SavingsGoal;
}
 
const props = defineProps<Props>();
 
const progressPercentage = computed(() => {
  return (props.goal.currentAmount / props.goal.targetAmount) * 100;
});
 
const remainingAmount = computed(() => {
  return props.goal.targetAmount - props.goal.currentAmount;
});
 
const estimatedCompletionDate = computed(() => {
  const dailyAverage = calculateDailyAverage(props.goal);
  if (dailyAverage <= 0) return '无法预估';
  
  const daysRemaining = Math.ceil(remainingAmount.value / dailyAverage);
  const completionDate = new Date(Date.now() + daysRemaining * 24 * 60 * 60 * 1000);
  
  return completionDate.toLocaleDateString();
});
 
function calculateDailyAverage(goal: SavingsGoal): number {
  const daysElapsed = Math.max(1, Math.floor((Date.now() - goal.startDate) / (24 * 60 * 60 * 1000)));
  return goal.currentAmount / daysElapsed;
}
</script>

🔄 状态管理

Pinia Store

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

实现示例:

// 储蓄游戏状态管理
export const useSavingsGameStore = defineStore('savingsGame', () => {
  // 状态
  const goals = ref<SavingsGoal[]>([]);
  const achievements = ref<UserAchievement[]>([]);
  const virtualWallet = ref<VirtualWallet | null>(null);
  const stockPositions = ref<StockPosition[]>([]);
  const leaderboard = ref<Leaderboard | null>(null);
  const isLoading = ref(false);
  
  // 计算属性
  const activeGoals = computed(() => {
    return goals.value.filter(goal => goal.status === GoalStatus.ACTIVE);
  });
  
  const completedGoals = computed(() => {
    return goals.value.filter(goal => goal.status === GoalStatus.COMPLETED);
  });
  
  const totalSavings = computed(() => {
    return goals.value.reduce((sum, goal) => sum + goal.currentAmount, 0);
  });
  
  const portfolioValue = computed(() => {
    return stockPositions.value.reduce((sum, position) => {
      return sum + (position.quantity * position.currentPrice);
    }, 0);
  });
  
  // 操作
  const fetchGoals = async (): Promise<void> => {
    isLoading.value = true;
    try {
      const goalList = await savingsGameService.getGoals();
      goals.value = goalList;
    } catch (error) {
      console.error('获取目标失败:', error);
      throw error;
    } finally {
      isLoading.value = false;
    }
  };
  
  const createGoal = async (data: CreateSavingsGoalRequest): Promise<SavingsGoal> => {
    try {
      const goal = await savingsGameService.createGoal(data);
      goals.value.unshift(goal);
      return goal;
    } catch (error) {
      console.error('创建目标失败:', error);
      throw error;
    }
  };
  
  const updateGoalProgress = async (goalId: string, amount: number): Promise<void> => {
    try {
      await savingsGameService.updateGoalProgress(goalId, amount);
      await fetchGoals(); // 重新获取目标列表
    } catch (error) {
      console.error('更新目标进度失败:', error);
      throw error;
    }
  };
  
  const fetchAchievements = async (): Promise<void> => {
    try {
      const achievementList = await achievementService.getUserAchievements();
      achievements.value = achievementList;
    } catch (error) {
      console.error('获取成就失败:', error);
      throw error;
    }
  };
  
  const fetchVirtualWallet = async (): Promise<void> => {
    try {
      const wallet = await savingsGameService.getVirtualWallet();
      virtualWallet.value = wallet;
    } catch (error) {
      console.error('获取虚拟钱包失败:', error);
      throw error;
    }
  };
  
  const fetchStockPositions = async (): Promise<void> => {
    try {
      const positions = await savingsGameService.getStockPositions();
      stockPositions.value = positions;
    } catch (error) {
      console.error('获取持仓失败:', error);
      throw error;
    }
  };
  
  const fetchLeaderboard = async (type: LeaderboardType, period: LeaderboardPeriod): Promise<void> => {
    try {
      const board = await savingsGameService.getLeaderboard(type, period);
      leaderboard.value = board;
    } catch (error) {
      console.error('获取排行榜失败:', error);
      throw error;
    }
  };
  
  return {
    goals,
    achievements,
    virtualWallet,
    stockPositions,
    leaderboard,
    isLoading,
    activeGoals,
    completedGoals,
    totalSavings,
    portfolioValue,
    fetchGoals,
    createGoal,
    updateGoalProgress,
    fetchAchievements,
    fetchVirtualWallet,
    fetchStockPositions,
    fetchLeaderboard
  };
});

🧪 测试

单元测试

功能描述: 储蓄游戏模块的单元测试

实现示例:

// 储蓄目标测试
describe('SavingsGoal', () => {
  it('should create savings goal successfully', async () => {
    const goalData = {
      title: '旅行基金',
      description: '存钱去日本旅行',
      targetAmount: 10000,
      startDate: Date.now(),
      endDate: Date.now() + 365 * 24 * 60 * 60 * 1000,
      category: GoalCategory.VACATION
    };
    
    const goal = await createSavingsGoal(goalData);
    
    expect(goal.id).toBeDefined();
    expect(goal.title).toBe(goalData.title);
    expect(goal.targetAmount).toBe(goalData.targetAmount * 100);
    expect(goal.currentAmount).toBe(0);
    expect(goal.status).toBe(GoalStatus.ACTIVE);
  });
  
  it('should update goal progress correctly', async () => {
    const goal = await createTestGoal();
    const amount = 1000;
    
    const updatedGoal = await updateGoalProgress(goal.id, amount);
    
    expect(updatedGoal.currentAmount).toBe(amount);
  });
  
  it('should complete goal when target reached', async () => {
    const goal = await createTestGoal();
    
    await updateGoalProgress(goal.id, goal.targetAmount);
    const updatedGoal = await getGoal(goal.id);
    
    expect(updatedGoal.status).toBe(GoalStatus.COMPLETED);
  });
});
 
// 虚拟交易测试
describe('VirtualTrading', () => {
  it('should buy stock successfully', async () => {
    const stock = await createTestStock();
    const buyData = {
      stockId: stock.id,
      quantity: 100,
      userId: 'test-user'
    };
    
    const transaction = await buyStock(buyData);
    
    expect(transaction.id).toBeDefined();
    expect(transaction.type).toBe(TransactionType.BUY);
    expect(transaction.quantity).toBe(buyData.quantity);
    expect(transaction.status).toBe(TransactionStatus.COMPLETED);
  });
  
  it('should sell stock successfully', async () => {
    const stock = await createTestStock();
    const position = await createTestPosition(stock.id, 100);
    
    const sellData = {
      stockId: stock.id,
      quantity: 50,
      userId: 'test-user'
    };
    
    const transaction = await sellStock(sellData);
    
    expect(transaction.type).toBe(TransactionType.SELL);
    expect(transaction.quantity).toBe(sellData.quantity);
  });
  
  it('should reject buy order when insufficient balance', async () => {
    const stock = await createTestStock({ currentPrice: 10000 });
    const buyData = {
      stockId: stock.id,
      quantity: 1000, // 需要100万虚拟币
      userId: 'test-user'
    };
    
    await expect(buyStock(buyData)).rejects.toThrow('虚拟钱包余额不足');
  });
});
 
// 成就系统测试
describe('AchievementSystem', () => {
  it('should complete achievement when condition met', async () => {
    const achievement = await createTestAchievement({
      condition: { type: 'total_savings', target: 10000 }
    });
    
    // 模拟储蓄达到目标
    await updateGoalProgress('test-goal', 10000);
    
    await checkAchievementProgress('test-user');
    
    const userAchievement = await getUserAchievement('test-user', achievement.id);
    expect(userAchievement.isCompleted).toBe(true);
  });
});

📚 最佳实践

1. 游戏化设计

  • 设定合理的目标和奖励机制
  • 提供即时反馈和进度可视化
  • 平衡挑战性和可达性
  • 鼓励长期参与而非短期刷分

2. 用户体验

  • 简化操作流程,降低学习成本
  • 提供清晰的目标和进度展示
  • 支持个性化设置和自定义
  • 及时的错误提示和帮助信息

3. 数据安全

  • 虚拟货币和真实货币严格分离
  • 防止刷分和作弊行为
  • 保护用户隐私和敏感信息
  • 定期数据备份和恢复

4. 性能优化

  • 缓存常用数据,减少API调用
  • 异步处理复杂计算
  • 优化数据库查询和索引
  • 实现增量更新和懒加载

Penny Lens 储蓄游戏模块 - 让储蓄变得有趣和激励 🎮