feat: 新增站内消息系统,支持通知与订单跳转
This commit is contained in:
1 parent
411ec8be23
commit
c799b62f6b
14 files changed
+464
-9
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Android 前台服务开关:让 App 在退到后台后仍保持进程存活,
|
||||
/// 从而继续轮询站内消息并弹系统通知。
|
||||
///
|
||||
/// 注意:这不是推送。用户手动「强行停止」/清理后台后服务会终止,
|
||||
/// 需要被杀进程后仍送达时必须接入厂商推送或极光/个推。
|
||||
class BackgroundKeepAlive {
|
||||
BackgroundKeepAlive._();
|
||||
|
||||
static const _channel = MethodChannel('voltlink/keepalive');
|
||||
|
||||
static Future<void> setEnabled(bool enabled) async {
|
||||
if (!Platform.isAndroid) return;
|
||||
try {
|
||||
await _channel.invokeMethod(enabled ? 'start' : 'stop');
|
||||
} on PlatformException {
|
||||
// 无通知权限或系统限制时静默失败,不影响主流程。
|
||||
} on MissingPluginException {
|
||||
// 非 Android 宿主或插件未注册。
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
/// 本地系统通知。Android 专用(当前电工端只在 Android 分发)。
|
||||
///
|
||||
/// 只负责「展示通知」与「回传点击 payload」,消息从哪来由调用方决定。
|
||||
class LocalNotifications {
|
||||
LocalNotifications._();
|
||||
|
||||
static final instance = LocalNotifications._();
|
||||
|
||||
static const channelId = 'voltlink_dispatch';
|
||||
static const channelName = '派单与站内消息';
|
||||
static const channelDesc = '收到新的派单或站内消息时提醒';
|
||||
|
||||
final _plugin = FlutterLocalNotificationsPlugin();
|
||||
final _taps = StreamController<String>.broadcast();
|
||||
|
||||
/// 冷启动时由通知拉起的路由(初始化完成时读取一次)。
|
||||
String? _launchRoute;
|
||||
|
||||
bool _ready = false;
|
||||
|
||||
/// 运行期点击通知的路由流。
|
||||
Stream<String> get taps => _taps.stream;
|
||||
|
||||
/// 在 `runApp` 之前调用。
|
||||
Future<void> init() async {
|
||||
if (_ready || !Platform.isAndroid) return;
|
||||
const settings = InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
);
|
||||
await _plugin.initialize(
|
||||
settings: settings,
|
||||
onDidReceiveNotificationResponse: _onTap,
|
||||
);
|
||||
|
||||
final android = _plugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin
|
||||
>();
|
||||
// Android 13+ 需要用户授权,否则通知不会展示
|
||||
await android?.requestNotificationsPermission();
|
||||
await android?.createNotificationChannel(
|
||||
const AndroidNotificationChannel(
|
||||
channelId,
|
||||
channelName,
|
||||
description: channelDesc,
|
||||
importance: Importance.high,
|
||||
),
|
||||
);
|
||||
|
||||
final launch = await _plugin.getNotificationAppLaunchDetails();
|
||||
if ((launch?.didNotificationLaunchApp ?? false)) {
|
||||
final payload = launch?.notificationResponse?.payload;
|
||||
if (payload != null && payload.isNotEmpty) _launchRoute = payload;
|
||||
}
|
||||
_ready = true;
|
||||
}
|
||||
|
||||
/// 取出冷启动通知携带的路由(只返回一次)。
|
||||
String? takeLaunchRoute() {
|
||||
final route = _launchRoute;
|
||||
_launchRoute = null;
|
||||
return route;
|
||||
}
|
||||
|
||||
Future<void> show({
|
||||
required int id,
|
||||
required String title,
|
||||
required String body,
|
||||
required String route,
|
||||
}) async {
|
||||
if (!_ready) return;
|
||||
await _plugin.show(
|
||||
id: id,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
channelId,
|
||||
channelName,
|
||||
channelDescription: channelDesc,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
),
|
||||
),
|
||||
payload: route,
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap(NotificationResponse response) {
|
||||
final payload = response.payload;
|
||||
if (payload != null && payload.isNotEmpty) _taps.add(payload);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class TokenStorage {
|
||||
static const _kUserInfo = 'userInfo';
|
||||
static const _kServerHost = 'serverHost';
|
||||
static const _kServerPort = 'serverPort';
|
||||
static const _kNoticeAlertMaxId = 'noticeAlertMaxId';
|
||||
|
||||
static Future<TokenStorage> open() async =>
|
||||
TokenStorage(await SharedPreferences.getInstance());
|
||||
@@ -55,5 +56,11 @@ class TokenStorage {
|
||||
await _prefs.remove(_kUserInfo);
|
||||
}
|
||||
|
||||
/// 已弹过系统通知的最大消息 id,避免重启后重复提醒历史消息。
|
||||
int get noticeAlertMaxId => _prefs.getInt(_kNoticeAlertMaxId) ?? -1;
|
||||
|
||||
Future<void> setNoticeAlertMaxId(int id) =>
|
||||
_prefs.setInt(_kNoticeAlertMaxId, id);
|
||||
|
||||
bool get hasSession => accessToken.isNotEmpty;
|
||||
}
|
||||
Reference in new issue
Block a user