feat(mobile-app): 添加待办操作轮询机制
解决老版本 App 升级后不重启导致无法激活待办事项的问题。 - 新增 PendingActionPollingService 定时轮询服务(每4秒检查) - App启动时无待办则启动轮询,有待办则直接进入待办页面 - 轮询检测到待办后自动停止并跳转,防止重入问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b3342de5c
commit
2136b7a144
|
|
@ -587,7 +587,8 @@
|
|||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): convert BigInt to string for JSON serialization in getUnprocessedSettlements\n\nThe entry.id field is BigInt type from Prisma which cannot be JSON serialized directly.\nConvert to string for API response and back to BigInt when storing to database.\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): improve empty state display for offline settlement deduction\n\nWhen there are no settlement records to deduct, show a more informative message:\n- If user has balance from deposits/transfers: explain it''s not from earnings\n- If user has no balance: explain there are no settlement records\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet/blockchain\\): 热钱包余额预检查及接收方钱包自动创建\n\n1. blockchain-service: 新增热钱包 dUSDT 余额定时更新调度器\n - 每 5 秒查询热钱包在 KAVA 链上的 dUSDT 余额\n - 更新到 Redis DB 0,key 格式: hot_wallet:dusdt_balance:{chainType}\n - TTL 30 秒,服务故障时缓存自动过期\n\n2. wallet-service: 新增热钱包余额缓存服务\n - 从 Redis DB 0 读取热钱包余额缓存\n - 严格模式:无法获取余额或余额不足时拒绝转账\n - 提示信息:\"财务系统审计中,请稍后再试\"\n\n3. wallet-service: 转账确认时自动创建接收方钱包\n - 解决接收方钱包不存在导致入账失败的问题\n - 使用 upsert 避免并发创建冲突\n - 在同一事务中完成创建和入账\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet/blockchain\\): 热钱包余额预检查及接收方钱包自动创建\n\n1. blockchain-service: 新增热钱包 dUSDT 余额定时更新调度器\n - 每 5 秒查询热钱包在 KAVA 链上的 dUSDT 余额\n - 更新到 Redis DB 0,key 格式: hot_wallet:dusdt_balance:{chainType}\n - TTL 30 秒,服务故障时缓存自动过期\n\n2. wallet-service: 新增热钱包余额缓存服务\n - 从 Redis DB 0 读取热钱包余额缓存\n - 严格模式:无法获取余额或余额不足时拒绝转账\n - 提示信息:\"财务系统审计中,请稍后再试\"\n\n3. wallet-service: 转账确认时自动创建接收方钱包\n - 解决接收方钱包不存在导致入账失败的问题\n - 使用 upsert 避免并发创建冲突\n - 在同一事务中完成创建和入账\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 添加内部转账入账修复脚本\n\n新增一次性修复脚本用于补录因接收方钱包未创建导致入账失败的内部转账。\n\n脚本特性:\n- DRY_RUN 模式:默认只检查不执行,需手动改为 false 才真正修复\n- 完整验证:订单状态、类型、接收方信息、txHash\n- 幂等性检查:确认接收方没有 TRANSFER_IN 流水\n- 转出方验证:确认转出方有 TRANSFER_OUT 流水(已扣款)\n- 乐观锁:使用 version 字段防止并发修改\n- 审计追踪:payloadJson.dataFix=true 标记修复操作\n- 详细日志:每步操作都有时间戳和日志级别\n\n使用方法:\n1. 在 wallet-service 容器内执行 DRY_RUN 检查\n2. 确认无误后将 DRY_RUN 改为 false 再次执行\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import '../services/contract_signing_service.dart';
|
|||
import '../services/contract_check_service.dart';
|
||||
import '../services/pending_action_service.dart';
|
||||
import '../services/pending_action_check_service.dart';
|
||||
import '../services/pending_action_polling_service.dart';
|
||||
import '../telemetry/storage/telemetry_storage.dart';
|
||||
import '../../features/kyc/data/kyc_service.dart';
|
||||
|
||||
|
|
@ -146,6 +147,12 @@ final pendingActionCheckServiceProvider = Provider<PendingActionCheckService>((r
|
|||
return PendingActionCheckService(pendingActionService: pendingActionService);
|
||||
});
|
||||
|
||||
// Pending Action Polling Service Provider (用于轮询检查待办操作)
|
||||
final pendingActionPollingServiceProvider = Provider<PendingActionPollingService>((ref) {
|
||||
final checkService = ref.watch(pendingActionCheckServiceProvider);
|
||||
return PendingActionPollingService(checkService: checkService);
|
||||
});
|
||||
|
||||
// Override provider with initialized instance
|
||||
ProviderContainer createProviderContainer(LocalStorage localStorage) {
|
||||
return ProviderContainer(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'pending_action_check_service.dart';
|
||||
|
||||
/// 待办操作轮询回调
|
||||
typedef PendingActionDetectedCallback = void Function();
|
||||
|
||||
/// 待办操作轮询服务
|
||||
///
|
||||
/// 用于在 App 启动后定时检查是否有新的待办操作。
|
||||
///
|
||||
/// 设计说明:
|
||||
/// - App 启动时如果检测到待办,直接进入待办页面,不启动轮询
|
||||
/// - App 启动时如果没有待办,启动轮询器定时检查(每 4 秒)
|
||||
/// - 轮询检测到待办时,停止轮询并触发回调
|
||||
/// - 待办完成后不会重启轮询(防止重入问题)
|
||||
class PendingActionPollingService {
|
||||
final PendingActionCheckService _checkService;
|
||||
|
||||
/// 轮询间隔(秒)
|
||||
static const int _pollingIntervalSeconds = 4;
|
||||
|
||||
/// 轮询定时器
|
||||
Timer? _pollingTimer;
|
||||
|
||||
/// 是否正在运行
|
||||
bool _isRunning = false;
|
||||
|
||||
/// 检测到待办时的回调
|
||||
PendingActionDetectedCallback? _onPendingActionDetected;
|
||||
|
||||
PendingActionPollingService({
|
||||
required PendingActionCheckService checkService,
|
||||
}) : _checkService = checkService;
|
||||
|
||||
/// 是否正在运行
|
||||
bool get isRunning => _isRunning;
|
||||
|
||||
/// 启动轮询
|
||||
///
|
||||
/// [onPendingActionDetected] 检测到待办操作时的回调
|
||||
void start({required PendingActionDetectedCallback onPendingActionDetected}) {
|
||||
if (_isRunning) {
|
||||
debugPrint('[PendingActionPollingService] 轮询已在运行中,忽略重复启动');
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_onPendingActionDetected = onPendingActionDetected;
|
||||
|
||||
debugPrint('[PendingActionPollingService] 启动轮询,间隔: $_pollingIntervalSeconds 秒');
|
||||
|
||||
_pollingTimer = Timer.periodic(
|
||||
const Duration(seconds: _pollingIntervalSeconds),
|
||||
(_) => _checkForPendingActions(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 停止轮询
|
||||
void stop() {
|
||||
if (!_isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[PendingActionPollingService] 停止轮询');
|
||||
|
||||
_pollingTimer?.cancel();
|
||||
_pollingTimer = null;
|
||||
_isRunning = false;
|
||||
_onPendingActionDetected = null;
|
||||
}
|
||||
|
||||
/// 执行一次检查
|
||||
Future<void> _checkForPendingActions() async {
|
||||
if (!_isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugPrint('[PendingActionPollingService] 执行轮询检查...');
|
||||
|
||||
final hasPending = await _checkService.hasPendingActions();
|
||||
|
||||
if (hasPending && _isRunning) {
|
||||
debugPrint('[PendingActionPollingService] 检测到待办操作!停止轮询并触发回调');
|
||||
|
||||
// 先停止轮询,防止重入
|
||||
final callback = _onPendingActionDetected;
|
||||
stop();
|
||||
|
||||
// 触发回调
|
||||
callback?.call();
|
||||
} else {
|
||||
debugPrint('[PendingActionPollingService] 无待办操作');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PendingActionPollingService] 检查失败: $e');
|
||||
// 检查失败不停止轮询,继续下一次检查
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import '../../../../routes/route_paths.dart';
|
|||
import '../../../../bootstrap.dart';
|
||||
import '../../../../core/providers/maintenance_provider.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/pending_action_polling_service.dart';
|
||||
import '../../../../routes/app_router.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// 开屏页面 - 应用启动时显示的第一个页面
|
||||
|
|
@ -114,6 +116,33 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
|||
_navigateToNextPage();
|
||||
}
|
||||
|
||||
/// 启动待办操作轮询
|
||||
///
|
||||
/// 仅在启动时没有检测到待办操作时调用。
|
||||
/// 轮询检测到待办后会自动停止并跳转到待办页面。
|
||||
void _startPendingActionPolling() {
|
||||
final pollingService = ref.read(pendingActionPollingServiceProvider);
|
||||
|
||||
pollingService.start(
|
||||
onPendingActionDetected: () {
|
||||
debugPrint('[SplashPage] 轮询检测到待办操作 → 跳转到待办操作页面');
|
||||
// 使用 navigatorKey 进行跳转,因为此时 SplashPage 可能已经不在树上
|
||||
_navigateToPendingActions();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 跳转到待办操作页面
|
||||
void _navigateToPendingActions() {
|
||||
// 使用全局 rootNavigatorKey 进行跳转,因为此时 SplashPage 已不在 widget 树上
|
||||
final navigatorContext = rootNavigatorKey.currentContext;
|
||||
if (navigatorContext != null) {
|
||||
GoRouter.of(navigatorContext).go(RoutePaths.pendingActions);
|
||||
} else {
|
||||
debugPrint('[SplashPage] 无法获取 Navigator Context,跳转失败');
|
||||
}
|
||||
}
|
||||
|
||||
/// 导航到下一个页面
|
||||
Future<void> _navigateToNextPage() async {
|
||||
// 防止重复跳转
|
||||
|
|
@ -160,17 +189,19 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
|||
if (!mounted) return;
|
||||
|
||||
if (hasPending) {
|
||||
// 有待办操作,跳转到待办操作页面
|
||||
// 有待办操作,跳转到待办操作页面(不启动轮询)
|
||||
debugPrint('[SplashPage] 有待办操作 → 跳转到待办操作页面');
|
||||
context.go(RoutePaths.pendingActions);
|
||||
} else {
|
||||
// 无待办操作,跳转到主页面(龙虎榜)
|
||||
debugPrint('[SplashPage] 无待办操作 → 跳转到龙虎榜');
|
||||
// 无待办操作,启动轮询检查并跳转到主页面
|
||||
debugPrint('[SplashPage] 无待办操作 → 启动轮询并跳转到龙虎榜');
|
||||
_startPendingActionPolling();
|
||||
context.go(RoutePaths.ranking);
|
||||
}
|
||||
} catch (e) {
|
||||
// 检查失败,不阻止用户使用,直接跳转到主页面
|
||||
debugPrint('[SplashPage] 检查待办操作失败: $e → 跳转到龙虎榜');
|
||||
// 检查失败,不阻止用户使用,启动轮询并跳转到主页面
|
||||
debugPrint('[SplashPage] 检查待办操作失败: $e → 启动轮询并跳转到龙虎榜');
|
||||
_startPendingActionPolling();
|
||||
context.go(RoutePaths.ranking);
|
||||
}
|
||||
} else if (authState.isFirstLaunch) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue