Flutter for OpenHarmony:从零搭建幸运大转盘app(七)抽奖次数管理
免费抽奖不能无限次,不然就没意思了。这一篇我们来实现抽奖次数的管理:每天有固定次数,用完了要等恢复,也可以看广告换次数。

免费抽奖不能无限次,不然就没意思了。这一篇我们来实现抽奖次数的管理:每天有固定次数,用完了要等恢复,也可以看广告换次数。
设计次数规则
先定好规则:
- 每天最多抽5次
- 总次数上限10次(可以攒着)
- 用完后每30分钟恢复1次
- 看广告可以额外获得1次
这些规则在LotteryManager中用常量定义:
class LotteryManager extends ChangeNotifier {
static const int maxDailyChances = 5;
static const int maxTotalChances = 10;
static const Duration recoveryInterval = Duration(minutes: 30);
int _totalChances = 5;
int _dailyUsed = 0;
DateTime _lastRecoveryTime = DateTime.now();
DateTime _lastResetDate = DateTime.now();
Timer? _recoveryTimer;
}
_totalChances是当前可用次数,_dailyUsed是今天已经用了多少次。两个时间字段分别记录上次恢复时间和上次重置日期。
单例模式
次数管理器全局只需要一个实例,用单例模式实现:
static final LotteryManager _instance = LotteryManager._internal();
factory LotteryManager() => _instance;
LotteryManager._internal();
这样在任何地方调用LotteryManager()都会拿到同一个实例,数据不会乱。
判断能否抽奖
提供一个getter让外部判断当前能不能抽奖:
int get dailyRemaining => maxDailyChances - _dailyUsed;
bool get canSpin => _totalChances > 0 && dailyRemaining > 0;
两个条件都要满足:总次数大于0,今天剩余次数也大于0。
消耗次数
用户点击抽奖时,调用useChance方法:
bool useChance() {
_checkDailyReset();
if (!canSpin) return false;
_totalChances--;
_dailyUsed++;
_saveState();
notifyListeners();
return true;
}
先检查是否需要重置每日次数,然后判断能不能抽,能抽就扣除次数并保存状态。返回值告诉调用方是否成功。
每日重置
每天凌晨,今日已用次数要清零:
void _checkDailyReset() {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final lastReset = DateTime(_lastResetDate.year, _lastResetDate.month, _lastResetDate.day);
if (today.isAfter(lastReset)) {
_dailyUsed = 0;
_lastResetDate = now;
_saveState();
notifyListeners();
}
}
比较的是日期而不是时间戳,所以用DateTime(year, month, day)只保留年月日。如果今天比上次重置日期晚,就执行重置。
自动恢复次数
次数用完后,每30分钟恢复1次。用定时器实现:
void _startRecoveryTimer() {
_recoveryTimer?.cancel();
_recoveryTimer = Timer.periodic(const Duration(seconds: 1), (_) {
_checkDailyReset();
_tryRecover();
if (_totalChances < maxTotalChances) {
notifyListeners();
}
});
}
每秒检查一次,如果到了恢复时间就增加次数。为什么每秒都检查?因为UI上要显示倒计时,需要实时更新。
恢复逻辑在_tryRecover方法中:
void _tryRecover() {
if (_totalChances >= maxTotalChances) return;
final now = DateTime.now();
final elapsed = now.difference(_lastRecoveryTime);
if (elapsed >= recoveryInterval) {
final recoveryCount = elapsed.inMinutes ~/ recoveryInterval.inMinutes;
_totalChances = (_totalChances + recoveryCount).clamp(0, maxTotalChances);
_lastRecoveryTime = now;
_saveState();
notifyListeners();
}
}
如果已经满了就不恢复。计算距离上次恢复过了多久,如果超过30分钟就恢复相应的次数。clamp确保不会超过上限。
计算恢复倒计时
UI上需要显示距离下次恢复还有多久:
Duration get timeUntilNextRecovery {
if (_totalChances >= maxTotalChances) return Duration.zero;
final nextRecovery = _lastRecoveryTime.add(recoveryInterval);
final remaining = nextRecovery.difference(DateTime.now());
return remaining.isNegative ? Duration.zero : remaining;
}
如果已经满了返回0,否则计算下次恢复时间减去当前时间。
看广告获得次数
提供一个方法让用户通过看广告获得额外次数:
void watchAdForChance() {
addChances(1);
}
void addChances(int count) {
_totalChances = (_totalChances + count).clamp(0, maxTotalChances);
_saveState();
notifyListeners();
}
实际项目中,watchAdForChance应该先调用广告SDK,确认用户看完了再加次数。这里简化处理,直接加。
在UI中显示次数
在转盘页面顶部显示当前次数状态:
Widget _buildChanceIndicator() {
final remaining = _lotteryManager.timeUntilNextRecovery;
final minutes = remaining.inMinutes;
final seconds = remaining.inSeconds % 60;
final isFull = _lotteryManager.totalChances >= LotteryManager.maxTotalChances;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2)),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 总次数
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.confirmation_number, color: Colors.amber, size: 18),
const SizedBox(width: 4),
Text(
'${_lotteryManager.totalChances}/${LotteryManager.maxTotalChances}',
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.amber),
),
],
),
),
const SizedBox(width: 12),
// 今日剩余
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(Icons.today, color: Colors.blue, size: 18),
const SizedBox(width: 4),
Text(
'今日 ${_lotteryManager.dailyRemaining}/${LotteryManager.maxDailyChances}',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue.shade700),
),
],
),
),
// 恢复倒计时
if (!isFull && remaining > Duration.zero) ...[
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.timer, color: Colors.green.shade600, size: 18),
const SizedBox(width: 4),
Text(
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green.shade700),
),
],
),
),
],
],
),
);
}
三个小标签分别显示:总次数、今日剩余、恢复倒计时。倒计时只在次数未满时显示。
次数不足提示
用户点击抽奖但次数不足时,弹出提示:
void _showNoChanceDialog() {
final remaining = _lotteryManager.timeUntilNextRecovery;
final minutes = remaining.inMinutes;
final seconds = remaining.inSeconds % 60;
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Row(
children: [
Icon(Icons.hourglass_empty, color: Colors.orange),
SizedBox(width: 8),
Text('次数不足'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_lotteryManager.dailyRemaining <= 0)
const Text('今日抽奖次数已用完,明天再来吧!')
else ...[
Text('当前剩余 ${_lotteryManager.totalChances} 次抽奖机会'),
const SizedBox(height: 8),
if (remaining > Duration.zero)
Text('下次恢复: ${minutes}分${seconds}秒后', style: TextStyle(color: Colors.grey.shade600)),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('知道了'),
),
ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_watchAdForChance();
},
icon: const Icon(Icons.play_circle_outline, size: 18),
label: const Text('看广告得次数'),
),
],
),
);
}
弹窗里区分两种情况:今日次数用完和总次数用完。同时提供"看广告"的选项,给用户一个获取次数的途径。
模拟看广告
看广告的流程简化为一个2秒的加载动画:
void _watchAdForChance() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('广告加载中...'),
],
),
),
);
Future.delayed(const Duration(seconds: 2), () {
Navigator.pop(context);
_lotteryManager.watchAdForChance();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('🎉 获得1次抽奖机会!'), backgroundColor: Colors.green),
);
});
}
实际项目中要接入真正的广告SDK,这里只是演示流程。
监听数据变化
在initState中注册监听,数据变化时刷新UI:
void initState() {
super.initState();
_lotteryManager.addListener(_onManagerChanged);
}
void _onManagerChanged() {
if (mounted) setState(() {});
}
void dispose() {
_lotteryManager.removeListener(_onManagerChanged);
super.dispose();
}
mounted检查是为了避免组件已销毁时调用setState导致报错。
抽奖次数管理就完成了。用户每天有固定次数,用完了可以等恢复或者看广告。下一篇我们来实现数据持久化,让这些数据在应用重启后还能保留。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)