diff --git a/flutter_monisuo/PUSH_NOTIFICATION_GUIDE.md b/flutter_monisuo/PUSH_NOTIFICATION_GUIDE.md new file mode 100644 index 0000000..d04a80c --- /dev/null +++ b/flutter_monisuo/PUSH_NOTIFICATION_GUIDE.md @@ -0,0 +1,328 @@ +# Flutter前端推送通知集成方案 + +## 技术选型 + +### 推送服务选择 +考虑到国内环境,推荐使用 **极光推送 (JPush)** 或 **个推 (Getui)** + +**优势**: +- 国内推送到达率高 +- 支持iOS和Android +- 提供完善的Flutter SDK +- 免费版本足够使用 + +## 集成步骤 + +### 1. 添加依赖 + +```yaml +dependencies: + # 极光推送 + jpush_flutter: ^2.5.0 + + # 本地通知 + flutter_local_notifications: ^16.0.0 + + # 权限管理 + permission_handler: ^11.0.0 +``` + +### 2. Android配置 + +**android/app/build.gradle**: +```gradle +android { + defaultConfig { + manifestPlaceholders = [ + JPUSH_PKGNAME: applicationId, + JPUSH_APPKEY: "你的APPKEY", + JPUSH_CHANNEL: "developer-default" + ] + } +} +``` + +**android/app/src/main/AndroidManifest.xml**: +```xml + + + + + + + + + +``` + +### 3. iOS配置 + +**ios/Runner/Info.plist**: +```xml +UIBackgroundModes + + remote-notification + +``` + +### 4. 初始化推送 + +**lib/services/push_service.dart**: +```dart +import 'package:jpush_flutter/jpush_flutter.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class PushService { + static final PushService _instance = PushService._internal(); + factory PushService() => _instance; + PushService._internal(); + + final JPush jpush = JPush(); + final FlutterLocalNotificationsPlugin localNotifications = + FlutterLocalNotificationsPlugin(); + + Future init() async { + // 初始化极光推送 + jpush.addEventHandler( + onReceiveNotification: (Map message) async { + print('收到推送: $message'); + _handleNotification(message); + }, + onOpenNotification: (Map message) async { + print('点击推送: $message'); + _handleNotificationTap(message); + }, + ); + + jpush.setup( + appKey: '你的APPKEY', + channel: 'developer-default', + production: false, + debug: true, + ); + + // 初始化本地通知 + await _initLocalNotifications(); + } + + Future _initLocalNotifications() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings(); + + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + + await localNotifications.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (NotificationResponse response) { + _handleLocalNotificationTap(response.payload); + }, + ); + } + + void _handleNotification(Map message) { + // 处理推送消息 + final title = message['title'] ?? '新消息'; + final body = message['alert'] ?? ''; + + _showLocalNotification(title, body, message); + } + + void _handleNotificationTap(Map message) { + // 处理推送点击 + final type = message['extras']['type']; + + switch (type) { + case 'order': + // 跳转到订单详情 + break; + case 'asset': + // 跳转到资产页面 + break; + default: + // 跳转到首页 + break; + } + } + + Future _showLocalNotification( + String title, + String body, + Map data, + ) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'monisuo_channel', + '模拟所通知', + channelDescription: '模拟所应用通知', + importance: Importance.max, + priority: Priority.high, + ); + + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + + await localNotifications.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + title, + body, + platformChannelSpecifics, + payload: jsonEncode(data), + ); + } + + void _handleLocalNotificationTap(String? payload) { + if (payload != null) { + final data = jsonDecode(payload); + _handleNotificationTap(data); + } + } + + // 设置别名(通常是用户ID) + Future setAlias(String userId) async { + jpush.setAlias(userId); + } + + // 删除别名 + Future deleteAlias() async { + jpush.deleteAlias(); + } + + // 设置标签 + Future setTags(List tags) async { + jpush.setTags(tags); + } +} +``` + +### 5. 在main.dart中初始化 + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 初始化推送服务 + await PushService().init(); + + runApp(const MyApp()); +} +``` + +### 6. 登录后设置别名 + +```dart +// 用户登录成功后 +await PushService().setAlias(userId); + +// 用户退出登录后 +await PushService().deleteAlias(); +``` + +## 后端推送接口 + +**后端需要提供推送接口**: + +```java +@PostMapping("/api/push/send") +public Result sendPush( + @RequestParam String userId, + @RequestParam String title, + @RequestParam String body, + @RequestParam(required = false) Map extras +) { + // 调用极光推送API + JPushClient jpushClient = new JPushClient(appKey, masterSecret); + + PushPayload payload = PushPayload.newBuilder() + .setPlatform(Platform.all()) + .setAudience(Audience.alias(userId)) + .setNotification(Notification.alert(body)) + .setMessage(Message.content(body)) + .setOptions(Options.newBuilder().setApnsProduction(false).build()) + .build(); + + jpushClient.sendPush(payload); + + return Result.success("推送成功"); +} +``` + +## 推送场景 + +### 1. 充值审批通知 +```dart +{ + "title": "充值审批通过", + "body": "您的充值订单已审批通过,金额: 100 USDT", + "extras": { + "type": "order", + "orderNo": "F20260324001" + } +} +``` + +### 2. 提现审批通知 +```dart +{ + "title": "提现审批完成", + "body": "您的提现申请已处理,金额: 100 USDT", + "extras": { + "type": "order", + "orderNo": "F20260324002" + } +} +``` + +### 3. 资产变动通知 +```dart +{ + "title": "资产变动", + "body": "您的账户余额发生变动", + "extras": { + "type": "asset", + "userId": "123" + } +} +``` + +## 测试 + +### 1. 测试推送 +```dart +// 在开发环境中测试 +await PushService()._showLocalNotification( + '测试标题', + '测试内容', + {'type': 'test'}, +); +``` + +### 2. 测试极光推送 +- 在极光推送控制台发送测试推送 +- 检查应用是否收到推送 + +## 注意事项 + +1. **权限申请**: Android需要申请通知权限 +2. **iOS证书**: 需要配置APNs证书 +3. **测试环境**: 开发时使用开发环境 +4. **生产环境**: 上线前切换到生产环境 +5. **用户隐私**: 首次启动时请求推送权限 + +## 成本估算 + +- 极光推送免费版:每月100万条推送 +- 对于中小规模应用完全够用 +- 如需更多,可以升级付费版本 + +--- + +**集成时间**: 预计2-3小时 +**难度**: 中等 +**维护成本**: 低 diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart index 03ff639..8c0bcbb 100644 --- a/flutter_monisuo/lib/main.dart +++ b/flutter_monisuo/lib/main.dart @@ -13,6 +13,7 @@ import 'data/services/market_service.dart'; import 'data/services/trade_service.dart'; import 'data/services/asset_service.dart'; import 'data/services/fund_service.dart'; +import 'services/push_service.dart'; import 'providers/auth_provider.dart'; import 'providers/market_provider.dart'; import 'providers/asset_provider.dart'; @@ -26,6 +27,10 @@ void main() async { await SharedPreferences.getInstance(); await LocalStorage.init(); + + // 初始化推送服务 + await PushService().init(); + debugPrint('✅ 推送服务已初始化'); runApp(const MyApp()); } diff --git a/flutter_monisuo/lib/services/push_service.dart b/flutter_monisuo/lib/services/push_service.dart new file mode 100644 index 0000000..786afdd --- /dev/null +++ b/flutter_monisuo/lib/services/push_service.dart @@ -0,0 +1,321 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:jpush_flutter/jpush_flutter.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +/// 推送通知服务 +class PushService { + static final PushService _instance = PushService._internal(); + factory PushService() => _instance; + PushService._internal(); + + final JPush jpush = JPush(); + final FlutterLocalNotificationsPlugin localNotifications = + FlutterLocalNotificationsPlugin(); + + bool _initialized = false; + + /// 初始化推送服务 + Future init({String? appKey}) async { + if (_initialized) return; + + try { + // 初始化极光推送 + jpush.addEventHandler( + onReceiveNotification: (Map message) async { + debugPrint('📱 收到推送: $message'); + _handleNotification(message); + }, + onOpenNotification: (Map message) async { + debugPrint('📱 点击推送: $message'); + _handleNotificationTap(message); + }, + onReceiveMessage: (Map message) async { + debugPrint('📱 收到自定义消息: $message'); + }, + ); + + // 如果有appKey则设置,否则跳过(用于开发环境) + if (appKey != null && appKey.isNotEmpty) { + jpush.setup( + appKey: appKey, + channel: 'developer-default', + production: false, + debug: true, + ); + } + + // 初始化本地通知 + await _initLocalNotifications(); + + _initialized = true; + debugPrint('✅ 推送服务初始化成功'); + } catch (e) { + debugPrint('❌ 推送服务初始化失败: $e'); + } + } + + /// 初始化本地通知 + Future _initLocalNotifications() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + ); + + await localNotifications.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (NotificationResponse response) { + _handleLocalNotificationTap(response.payload); + }, + ); + + // 请求通知权限(Android 13+) + await localNotifications + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + } + + /// 处理推送消息 + void _handleNotification(Map message) { + try { + final extras = message['extras'] as Map?; + final title = extras?['title']?.toString() ?? + message['title']?.toString() ?? + '新消息'; + final body = extras?['body']?.toString() ?? + message['alert']?.toString() ?? + ''; + + _showLocalNotification(title, body, message); + } catch (e) { + debugPrint('❌ 处理推送消息失败: $e'); + } + } + + /// 处理推送点击 + void _handleNotificationTap(Map message) { + try { + final extras = message['extras'] as Map?; + final type = extras?['type']?.toString(); + + debugPrint('📱 推送类型: $type'); + + // TODO: 根据type跳转到对应页面 + switch (type) { + case 'order': + // 跳转到订单详情 + final orderNo = extras?['orderNo']?.toString(); + debugPrint('📱 跳转到订单: $orderNo'); + break; + case 'asset': + // 跳转到资产页面 + debugPrint('📱 跳转到资产页面'); + break; + default: + // 默认跳转到首页 + debugPrint('📱 默认跳转'); + break; + } + } catch (e) { + debugPrint('❌ 处理推送点击失败: $e'); + } + } + + /// 显示本地通知 + Future _showLocalNotification( + String title, + String body, + Map data, + ) async { + try { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'monisuo_channel', + '模拟所通知', + channelDescription: '模拟所应用通知', + importance: Importance.max, + priority: Priority.high, + showWhen: true, + enableVibration: true, + enableLights: true, + ); + + const DarwinNotificationDetails iOSPlatformChannelSpecifics = + DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const NotificationDetails platformChannelSpecifics = + NotificationDetails( + android: androidPlatformChannelSpecifics, + iOS: iOSPlatformChannelSpecifics, + ); + + await localNotifications.show( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + title, + body, + platformChannelSpecifics, + payload: jsonEncode(data), + ); + + debugPrint('✅ 本地通知显示成功: $title - $body'); + } catch (e) { + debugPrint('❌ 显示本地通知失败: $e'); + } + } + + /// 处理本地通知点击 + void _handleLocalNotificationTap(String? payload) { + if (payload != null) { + try { + final data = jsonDecode(payload) as Map; + _handleNotificationTap(data); + } catch (e) { + debugPrint('❌ 处理本地通知点击失败: $e'); + } + } + } + + /// 设置别名(通常是用户ID) + Future setAlias(String userId) async { + try { + if (!_initialized) { + debugPrint('⚠️ 推送服务未初始化,无法设置别名'); + return; + } + + jpush.setAlias(userId); + debugPrint('✅ 设置推送别名: $userId'); + } catch (e) { + debugPrint('❌ 设置别名失败: $e'); + } + } + + /// 删除别名 + Future deleteAlias() async { + try { + if (!_initialized) { + debugPrint('⚠️ 推送服务未初始化,无法删除别名'); + return; + } + + jpush.deleteAlias(); + debugPrint('✅ 删除推送别名'); + } catch (e) { + debugPrint('❌ 删除别名失败: $e'); + } + } + + /// 设置标签 + Future setTags(List tags) async { + try { + if (!_initialized) { + debugPrint('⚠️ 推送服务未初始化,无法设置标签'); + return; + } + + jpush.setTags(tags); + debugPrint('✅ 设置推送标签: $tags'); + } catch (e) { + debugPrint('❌ 设置标签失败: $e'); + } + } + + /// 获取注册ID + Future getRegistrationId() async { + try { + if (!_initialized) { + debugPrint('⚠️ 推送服务未初始化'); + return null; + } + + final rid = await jpush.getRegistrationId(); + debugPrint('📱 注册ID: $rid'); + return rid; + } catch (e) { + debugPrint('❌ 获取注册ID失败: $e'); + return null; + } + } + + /// 测试本地通知(用于开发环境) + Future testNotification() async { + await _showLocalNotification( + '测试通知', + '这是一条测试推送消息', + { + 'type': 'test', + 'title': '测试通知', + 'body': '这是一条测试推送消息', + }, + ); + } + + /// 发送充值审批通知 + Future sendDepositApprovalNotification({ + required String userId, + required String orderNo, + required String amount, + required bool approved, + }) async { + final title = approved ? '充值审批通过' : '充值审批驳回'; + final body = approved + ? '您的充值订单已审批通过,金额: $amount USDT' + : '您的充值订单已被驳回,金额: $amount USDT'; + + await _showLocalNotification(title, body, { + 'type': 'order', + 'orderNo': orderNo, + 'title': title, + 'body': body, + }); + } + + /// 发送提现审批通知 + Future sendWithdrawApprovalNotification({ + required String userId, + required String orderNo, + required String amount, + required bool approved, + }) async { + final title = approved ? '提现审批通过' : '提现审批驳回'; + final body = approved + ? '您的提现申请已处理,金额: $amount USDT' + : '您的提现申请已被驳回,金额: $amount USDT'; + + await _showLocalNotification(title, body, { + 'type': 'order', + 'orderNo': orderNo, + 'title': title, + 'body': body, + }); + } + + /// 发送资产变动通知 + Future sendAssetChangeNotification({ + required String userId, + required String message, + }) async { + await _showLocalNotification('资产变动', message, { + 'type': 'asset', + 'userId': userId, + 'title': '资产变动', + 'body': message, + }); + } +} diff --git a/flutter_monisuo/pubspec.yaml b/flutter_monisuo/pubspec.yaml index 623cbbc..6f62920 100644 --- a/flutter_monisuo/pubspec.yaml +++ b/flutter_monisuo/pubspec.yaml @@ -33,6 +33,11 @@ dependencies: # 字体 google_fonts: ^6.2.1 + # 推送通知 + jpush_flutter: ^2.5.0 + flutter_local_notifications: ^16.0.0 + permission_handler: ^11.0.0 + dev_dependencies: flutter_test: sdk: flutter