Commit 3fbc72de by ethanlamzs

Merge branch 'feature-2607-pushMsg' into feature-2607-iosbuild-ext

消息推送
2 parents a646ed94 0091711a
......@@ -47,3 +47,5 @@ assets/static
pubspec.lock
assets/dist.zip
macos/Podfile.lock
macos/Flutter/GeneratedPluginRegistrant.swift
windows/flutter/generated_plugins.cmake
......@@ -21,6 +21,8 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
// flutter_local_notifications 插件要求启用核心库脱糖
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
......@@ -91,6 +93,9 @@ flutter {
}
dependencies {
// flutter_local_notifications 插件要求启用核心库脱糖
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// 添加 AppCompat 支持库(PrivacyActivity需要)
implementation("androidx.appcompat:appcompat:1.6.1")
......@@ -104,7 +109,7 @@ dependencies {
// OPPO
//implementation("com.tencent.timpush:oppo:8.8.7357")
// vivo
//implementation("com.tencent.timpush:vivo:8.8.7357")
implementation("com.tencent.timpush:vivo:8.8.7357")
// Honor
//implementation("com.tencent.timpush:honor:8.7.7201")
// Meizu
......
{"version":"1.0.1","cn.banxe.bxe":{"manifestPlaceholders":{"VIVO_APPKEY":"f7e37b20f48234c425d9e7f82fdd16c0","VIVO_APPID":"105993977","HONOR_APPID":""},"vivoPushBussinessId":"44580","oppoPushBussinessId":"46190","oppoPushAppKey":"922926ff488b4c2cb76bb4103ca8a93d","oppoPushAppSecret":"30636ddc19d94571aa1378db15d61ac0"}}
\ No newline at end of file
{
"version": "1.0.1",
"cn.banxe.bxe": {
"manifestPlaceholders": {
"VIVO_APPKEY": "f7e37b20f48234c425d9e7f82fdd16c0",
"VIVO_APPID": "105993977",
"HONOR_APPID": ""
},
"vivoPushBussinessId": "44580"
}
}
\ No newline at end of file
......@@ -57,6 +57,9 @@ class WebState extends Equatable {
final int? userType;
final String? stuId;
/// 通知点击等场景需要直接打开的 URL
final String? targetUrl;
/// getOrientationCmd
final bool orientationCmdFlag;
final String orientationCmdMessage;
......@@ -99,6 +102,7 @@ class WebState extends Equatable {
this.classCode,
this.userType,
this.stuId,
this.targetUrl,
this.orientationCmdFlag = false,
this.orientationCmdMessage = '',
this.windowInfoCmdFlag = false,
......@@ -131,6 +135,7 @@ class WebState extends Equatable {
String? classCode,
int? userType,
String? stuId,
String? targetUrl,
bool? orientationCmdFlag,
String? orientationCmdMessage,
bool? windowInfoCmdFlag,
......@@ -162,6 +167,7 @@ class WebState extends Equatable {
classCode: classCode ?? this.classCode,
userType: userType ?? this.userType,
stuId: stuId ?? this.stuId,
targetUrl: targetUrl ?? this.targetUrl,
orientationCmdFlag: orientationCmdFlag ?? this.orientationCmdFlag,
orientationCmdMessage: orientationCmdMessage ?? this.orientationCmdMessage,
windowInfoCmdFlag: windowInfoCmdFlag ?? this.windowInfoCmdFlag,
......@@ -195,6 +201,7 @@ class WebState extends Equatable {
classCode,
userType,
stuId,
targetUrl,
orientationCmdFlag,
orientationCmdMessage,
windowInfoCmdFlag,
......@@ -394,6 +401,15 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
}
void _loadHtml() {
// 如果通过路由透传了 targetUrl(如通知点击场景),
// 优先在 WebView 中打开该 URL。
final targetUrl = state.targetUrl;
if (targetUrl != null && targetUrl.isNotEmpty) {
final String resolvedUrl = '${Constant.localServerUrl}/index.html#$targetUrl';
_controller.loadRequest(Uri.parse(resolvedUrl));
return;
}
var sharedPreferences = getIt.get<SharedPreferences>();
var debug = sharedPreferences.getInt('debug') ?? 0;
// 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况
......@@ -452,7 +468,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
imService.registerPush();
// 处理加群和退群
if (state.loginOpFlag) {
// imService.joinAndLeaveGroup(state.userCode!);
imService.joinAndLeaveGroup(state.userCode!);
}
} else {
debugPrint("缓存自动登录处,IM 登录失败");
......
......@@ -125,6 +125,6 @@ class Constant {
static const String hasShownPrivacyFirstTimeKey = 'has_shown_privacy_first_time';
/// 测试阶段使用
static const bool needIM = false;
static const bool needIM = true;
static const bool needUpgrade = true;
}
......@@ -50,6 +50,7 @@ import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/im_service.dart';
import 'package:appframe/services/local_server_service.dart';
import 'package:appframe/services/notification_service.dart';
import 'package:appframe/services/player_service.dart';
import 'package:appframe/services/recorder_service.dart';
import 'package:fluwx/fluwx.dart';
......@@ -241,6 +242,9 @@ Future<void> setupLocator() async {
instanceName: "appApiService",
);
/// 通知服务
getIt.registerSingleton<NotificationService>(NotificationService());
/// imService
getIt.registerSingleton<ImService>(ImService());
......
......@@ -18,7 +18,7 @@ class SubsRepository {
{
"type": type,
"userid": userid,
"classCode": classCode,
"classCodes": classCode,
},
);
return resp.statusCode == HttpStatus.ok ? resp.data : null;
......
import 'dart:convert';
import 'dart:io';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NotificationService {
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
/// 初始化通知插件
Future<void> initialize() async {
if (_isInitialized) return;
const androidSettings = AndroidInitializationSettings('@mipmap/launcher_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const settings = InitializationSettings(android: androidSettings, iOS: iosSettings);
final result = await _plugin.initialize(
settings,
onDidReceiveNotificationResponse: _onNotificationResponse,
);
_isInitialized = result ?? false;
if (_isInitialized) {
debugPrint('NotificationService 初始化成功');
// Android 13+ 需要请求通知权限
if (Platform.isAndroid) {
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
await androidPlugin?.requestNotificationsPermission();
}
} else {
debugPrint('NotificationService 初始化失败');
}
}
/// 处理通知点击(供外部调用,如离线推送点击)
void handleNotificationClick(String? payload) {
debugPrint('[notification_service] handleNotificationClick payload: $payload');
// 通过 /reload 中转路由跳转,确保即使当前已在 /web,
// GoRouter 也会完整重建 WebPage 并加载新的 targetUrl。
final targetUrl = _buildTargetUrl(payload);
debugPrint('[notification_service] targetUrl: $targetUrl');
if (targetUrl != null && targetUrl.isNotEmpty) {
router.go(
'/reload',
extra: {'targetUrl': targetUrl},
);
} else {
router.go('/web');
}
}
/// 前台本地通知点击回调处理
void _onNotificationResponse(NotificationResponse response) {
debugPrint('[notification_service] Notification clicked, payload: ${response.payload}');
handleNotificationClick(response.payload);
}
/// 解析通知 payload,结合 auth_roles 缓存拼接目标 URL。
/// payload 格式:{"func": "homework", "uniqueCode": "...", "classCode": "..."}
/// 返回示例:/h5/login/pages/applogin?sessionCode=xxx&userCode=xxx&classCode=xxx&userType=2&stuId=xxx&page=homeworkdetail&uniqueCode=xxx
String? _buildTargetUrl(String? payload) {
if (payload == null || payload.isEmpty) return null;
try {
final data = jsonDecode(payload) as Map<String, dynamic>;
final uniqueCode = data['uniqueCode'] as String?;
final classCode = data['classCode'] as String?;
if (uniqueCode == null || uniqueCode.isEmpty || classCode == null || classCode.isEmpty) {
debugPrint('[notification_service] payload 缺少 uniqueCode 或 classCode');
return null;
}
final prefs = getIt.get<SharedPreferences>();
final rolesJson = prefs.getString('auth_roles') ?? '';
if (rolesJson.isEmpty) {
debugPrint('[notification_service] auth_roles 缓存为空');
return null;
}
final roles = jsonDecode(rolesJson) as List<dynamic>;
Map<String, dynamic>? matchedRole;
for (final role in roles) {
if (role is Map<String, dynamic> && role['classCode']?.toString() == classCode) {
matchedRole = role;
break;
}
}
if (matchedRole == null) {
debugPrint('[notification_service] 未找到与 classCode=$classCode 匹配的 role');
return null;
}
final stuId = matchedRole['stuId']?.toString() ?? '';
final sessionCode = prefs.getString('auth_sessionCode') ?? '';
final userCode = prefs.getString('auth_userCode') ?? '';
return '/h5/login/pages/applogin?'
'sessionCode=$sessionCode&'
'userCode=$userCode&'
'classCode=$classCode&'
'userType=2&'
'stuId=$stuId&'
'page=homeworkdetail&'
'uniqueCode=$uniqueCode';
} catch (e, stackTrace) {
debugPrint('[notification_service] 解析 payload 失败: $e\n$stackTrace');
return null;
}
}
/// 显示手机通知(懒初始化:首次调用自动触发初始化)
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
// 懒初始化:首次调用时自动初始化
if (!_isInitialized) {
await initialize();
}
const androidDetails = AndroidNotificationDetails(
'homework_message_channel',
'作业消息通知',
channelDescription: '接收作业消息推送通知',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
await _plugin.show(id, title, body, details, payload: payload);
debugPrint('NotificationService showNotification: id=$id, title=$title, body=$body');
}
}
import 'package:appframe/config/routes.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
///
/// 用于重新加载的中间路由
/// 通知点击等场景先跳转到此页,再转发 targetUrl 到 /web,
/// 确保 GoRouter 完整重建 WebPage 并加载目标 URL。
///
class ReloadPage extends StatefulWidget {
const ReloadPage({super.key});
......@@ -23,7 +26,12 @@ class _ReloadPageState extends State<ReloadPage> {
}
void _performPostDisplayOperations() {
router.go('/web');
final extra = GoRouterState.of(context).extra;
if (extra is Map<String, dynamic> && extra['targetUrl'] != null) {
router.go('/web', extra: extra);
} else {
router.go('/web');
}
}
@override
......
......@@ -25,6 +25,7 @@ class WebPage extends StatelessWidget {
var classCode = extraData?['classCode'];
var userType = extraData?['userType'];
var stuId = extraData?['stuId'];
var targetUrl = extraData?['targetUrl'];
if (sessionCode == null || sessionCode == '') {
loginOpFlag = false;
......@@ -49,6 +50,7 @@ class WebPage extends StatelessWidget {
classCode: classCode,
userType: userType,
stuId: stuId,
targetUrl: targetUrl,
),
);
return webCubit;
......
import 'dart:convert';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
......@@ -89,6 +91,8 @@ class LoginUtil {
sharedPreferences.setString('auth_className', className);
sharedPreferences.setString('auth_stuName', stuName);
sharedPreferences.setString('auth_relation', relation);
// 用于处理点击推送通知时,获取跳转到指定页面的参数
sharedPreferences.setString('auth_roles', jsonEncode(roles));
if (loginType == 'router') {
router.go(
......
......@@ -12,6 +12,7 @@ import device_info_plus
import ffmpeg_kit_flutter_new
import file_picker
import flutter_image_compress_macos
import flutter_local_notifications
import flutter_localization
import flutter_sound
import geolocator_apple
......@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSoundPlugin.register(with: registry.registrar(forPlugin: "FlutterSoundPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
......
......@@ -97,6 +97,7 @@ dependencies:
cupertino_icons: ^1.0.8
fluttertoast: ^9.0.0
flutter_local_notifications: ^19.5.0
dev_dependencies:
flutter_test:
......
......@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!