✨ 我是 Muzi 的「文章捕手」,擅长在文字的星海中打捞精华。每当新的篇章诞生,我就会像整理贝壳一样,将思想的闪光点串成珍珠项链~
本文详细介绍了Cat Club项目中道具商店系统和每日签到系统的设计与实现。道具商店采用TabBar分类展示食物、特殊道具和配饰,商品卡片通过稀有度颜色区分,购买流程通过Firestore事务保证数据一致性。每日签到系统采用7天循环奖励机制,支持连续签到判断和奖励发放,UI设计优化了奖励展示避免溢出。宠物放生功能引入输入名称确认的二次确认机制,防止误操作。文章还总结了遇到的问题及解决方案,并展示了项目整体进度,体现了用户激励机制和游戏内经济平衡的设计思考,具备较高的实用价值和技术参考意义。
# 前言
今天是 Cat Club 项目开发的第四天,主要目标是完成道具商店系统和每日签到系统。这两个功能是养成类游戏的核心经济系统,让用户有动力每天打开应用,同时也为后续的道具消费提供了入口。
# 上午:道具商店系统
# 1. 商店页面架构
商店采用 TabBar + TabBarView 的经典分类结构,将道具分为三类:
- 食物:喂食宠物,恢复饱腹度
- 道具:特殊效果道具
- 配饰:装饰性道具(预留)
class ShopPage extends ConsumerStatefulWidget {
// ...
}
class _ShopPageState extends ConsumerState<ShopPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('道具商店'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '食物'),
Tab(text: '道具'),
Tab(text: '配饰'),
],
),
),
body: Column(
children: [
_buildCurrencyBar(coins, diamonds), // 货币栏
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildItemGrid(ItemCategory.food),
_buildItemGrid(ItemCategory.special),
_buildItemGrid(ItemCategory.accessory),
],
),
),
],
),
);
}
}
# 2. 商品卡片设计
每个商品卡片需要展示:
- 道具图标(根据类别显示不同图标)
- 稀有度标签(普通/稀有/史诗/传说)
- 名称和价格
稀有度通过颜色区分,让用户一眼就能识别道具价值:
Widget _buildItemCard(ItemModel item) {
final rarityColor = Color(item.rarityColorValue);
return Container(
decoration: BoxDecoration(
border: Border.all(color: rarityColor.withOpacity(0.3), width: 2),
boxShadow: [
BoxShadow(color: rarityColor.withOpacity(0.1), blurRadius: 8),
],
),
child: Column(
children: [
// 图标区域 + 稀有度标签
// 名称 + 价格
],
),
);
}
# 3. 购买流程
购买逻辑需要同时处理两件事:
- 扣除用户货币(金币或钻石)
- 将道具添加到背包
在 UserNotifier 中新增了 purchaseItem 方法:
Future<void> purchaseItem({
required String itemId,
required int price,
required CurrencyType currency,
}) async {
final userId = _getUserId();
if (userId == null) throw Exception('用户未登录');
// 使用 Firestore 事务确保原子性
await _firestoreService.purchaseItem(
userId: userId,
itemId: itemId,
price: price,
currency: currency,
);
}
# 下午:每日签到系统
# 1. 签到奖励配置
采用 7 天循环奖励机制,奖励逐日递增,第 7 天是大礼包:
| 天数 | 金币 | 钻石 | 道具 |
|---|---|---|---|
| 第1天 | 50 | - | - |
| 第2天 | 80 | - | - |
| 第3天 | 100 | - | 小鱼干 x2 |
| 第4天 | 120 | - | - |
| 第5天 | 150 | - | 猫零食 x2 |
| 第6天 | 180 | - | - |
| 第7天 | 300 | 10 | 高级鱼干 + 刷子 |
static const Map<int, CheckInReward> _rewardConfig = {
1: CheckInReward(coins: 50, description: '第1天'),
2: CheckInReward(coins: 80, description: '第2天'),
3: CheckInReward(coins: 100, items: {'food_fish': 2}, description: '第3天'),
// ...
7: CheckInReward(
coins: 300,
diamonds: 10,
items: {'food_premium_fish': 1, 'clean_brush': 1},
description: '第7天 - 周奖励',
),
};
# 2. 签到服务实现
签到的核心逻辑需要处理:
- 判断今天是否已签到
- 计算连续签到天数(断签则重置为 1)
- 发放对应奖励
使用 Firestore 事务保证数据一致性:
Future<CheckInResult> checkIn(String userId) async {
return _firestore.runTransaction<CheckInResult>((transaction) async {
final doc = await transaction.get(userRef);
final lastSignInDate = _parseDate(data['lastSignInDate']);
final currentDays = data['consecutiveDays'] as int? ?? 0;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
// 检查是否已签到
if (lastSignInDate != null) {
final lastDate = DateTime(
lastSignInDate.year,
lastSignInDate.month,
lastSignInDate.day,
);
if (lastDate == today) {
return CheckInResult.failure('今日已签到');
}
// 判断是否连续
final yesterday = today.subtract(const Duration(days: 1));
final isConsecutive = lastDate == yesterday;
final newDays = isConsecutive ? currentDays + 1 : 1;
// 获取奖励并更新
final reward = getRewardForDay(newDays);
transaction.update(userRef, {
'lastSignInDate': Timestamp.fromDate(now),
'consecutiveDays': newDays,
'coins': FieldValue.increment(reward.coins),
// ...
});
}
});
}
# 3. 签到对话框 UI
签到对话框需要展示 7 天的奖励预览,让用户看到坚持签到的好处:
Widget _buildCheckInContent(CheckInState state, List<CheckInReward> weeklyRewards) {
return Column(
children: [
// 标题
const Text('每日签到'),
// 连续签到天数
Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFFFF6B6B), Color(0xFFFF8E53)]),
),
child: Text('已连续签到 ${state.consecutiveDays} 天'),
),
// 7天奖励展示
Row(
children: List.generate(7, (index) {
return _buildDayCard(
day: index + 1,
reward: weeklyRewards[index],
isCompleted: /* 是否已完成 */,
isCurrent: /* 是否是今天 */,
);
}),
),
// 签到按钮
ElevatedButton(
onPressed: state.hasCheckedInToday ? null : _handleCheckIn,
child: Text(state.hasCheckedInToday ? '今日已签到' : '立即签到'),
),
],
);
}
# 傍晚:宠物放生功能
# 二次确认机制
放生是不可逆操作,需要严格的确认机制。我采用了"输入宠物名称确认"的方式:
class ReleaseConfirmDialog extends StatefulWidget {
final PetModel pet;
@override
State<ReleaseConfirmDialog> createState() => _ReleaseConfirmDialogState();
}
class _ReleaseConfirmDialogState extends State<ReleaseConfirmDialog> {
final _nameController = TextEditingController();
bool _isNameMatch = false;
void _checkNameMatch() {
setState(() {
_isNameMatch = _nameController.text.trim() == widget.pet.name;
});
}
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Column(
children: [
// 宠物信息展示
// 警告提示
Text('此操作不可恢复,宠物数据将永久删除'),
// 名称输入框
TextField(
controller: _nameController,
decoration: InputDecoration(
hintText: widget.pet.name,
suffixIcon: _isNameMatch
? Icon(Icons.check_circle, color: Colors.green)
: null,
),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: Text('取消')),
ElevatedButton(
onPressed: _isNameMatch ? () => Navigator.pop(context, true) : null,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('确认放生'),
),
],
);
}
}
# 今日成果
# 功能完成
- ✅ 道具商店系统(完整购买流程)
- ✅ 每日签到系统(7天循环奖励)
- ✅ 宠物放生功能(二次确认机制)
- ✅ 宠物切换栏(多宠物管理)
- ✅ 商店路由集成
# 新增文件
lib/
├── presentation/
│ ├── pages/
│ │ ├── shop/
│ │ │ └── shop_page.dart # 商店页面
│ │ └── home/
│ │ └── check_in_dialog.dart # 签到对话框
│ └── widgets/
│ └── pet/
│ ├── pet_selector.dart # 宠物切换栏
│ └── release_confirm_dialog.dart # 放生确认
├── providers/
│ └── check_in_provider.dart # 签到状态管理
└── services/
└── check_in_service.dart # 签到服务
# 遇到的问题
# 1. 签到对话框 7 天卡片溢出
问题:使用 GridView 展示 7 天奖励时,在小屏幕上会溢出。
解决方案:改用 Row + Expanded,让每个卡片自适应宽度:
Row(
children: List.generate(7, (index) {
return Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
// 卡片内容
),
);
}),
)
# 2. 连续签到天数计算
问题:需要正确区分"今天已签到"和"今天待签到"两种状态下的天数显示。
解决方案:
int currentDay;
if (state.consecutiveDays == 0) {
currentDay = 1; // 从未签到,当前是第1天
} else if (state.hasCheckedInToday) {
currentDay = ((state.consecutiveDays - 1) % 7) + 1; // 已签到
} else {
currentDay = (state.consecutiveDays % 7) + 1; // 待签到
}
# 明日计划
# 心得体会
今天完成了两个核心经济系统,让我对"如何设计用户激励机制"有了更深的理解。
签到系统看似简单,但细节很多:连续签到的判断逻辑、7天循环的计算、奖励的递增设计... 这些都需要仔细考虑用户体验。比如第7天的大礼包,就是为了让用户有坚持签到一周的动力。
商店系统让我思考了游戏内经济平衡的问题。道具定价需要考虑:获取难度(签到每天能拿多少金币)、使用频率(喂食道具消耗快)、稀有度(史诗道具要有明显优势)等因素。虽然现在只是简单的数值配置,但后续可以根据用户行为数据进行调整。
放生功能的二次确认机制也很有意思。输入宠物名称确认这个设计,既能防止误操作,又能让用户在输入时产生"不舍得"的情感,这种微妙的心理设计在产品中很常见。
# 项目进度
| 模块 | 进度 | 说明 |
|---|---|---|
| 认证系统 | 100% | 邮箱 + Google 登录 |
| 宠物养成 | 85% | 核心交互完成,缺动画 |
| 背包系统 | 100% | Firestore 持久化 |
| 商店系统 | 100% | 完整购买流程 |
| 签到系统 | 100% | 7天循环奖励 |
| 照片上传 | 60% | UI 完成,需启用 Storage |
| 社区功能 | 0% | 待开发 |
| AI 生成 | 10% | 接口预留 |
项目地址:Cat Club(待开源)