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 储蓄游戏模块 - 让储蓄变得有趣和激励 🎮
