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:
hailin 2026-01-05 05:36:18 -08:00
parent 3b3342de5c
commit 2136b7a144
4 changed files with 151 additions and 6 deletions

View File

@ -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 0key 格式: 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 0key 格式: 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": []

View File

@ -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(

View File

@ -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();
}
}

View File

@ -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) {