feat: 新增站内消息系统,支持通知与订单跳转

This commit is contained in:
andyesfly committed 2026-10-23 17:18:21 +08:00
1 parent 411ec8be23
commit c799b62f6b
14 files changed
+464 -9

No files matched your search

@@ -0,0 +1,86 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/api_response.dart';
import '../../../core/notifications/local_notifications.dart';
import '../../../core/providers.dart';
import '../../auth/application/session_controller.dart';
import 'notice_routes.dart';
/// 轮询未读站内消息,并在 App 不处于前台时弹出系统通知。
///
/// 登录后每 30s 拉一次未读列表;已提醒过的最大消息 id 持久化,
/// 重启后不会重复提醒历史消息。
final noticeAlertProvider =
NotifierProvider<NoticeAlertController, int>(NoticeAlertController.new);
class NoticeAlertController extends Notifier<int> {
static const _interval = Duration(seconds: 30);
Timer? _timer;
bool _busy = false;
@override
int build() {
_timer?.cancel();
_timer = null;
final loggedIn = ref.watch(sessionProvider.select((s) => s.isLoggedIn));
if (!loggedIn) return 0;
ref.onDispose(() {
_timer?.cancel();
_timer = null;
});
_timer = Timer.periodic(_interval, (_) => unawaited(_poll()));
unawaited(_poll());
return 0;
}
Future<void> _poll() async {
if (_busy) return;
_busy = true;
try {
final res = await ref
.read(noticeApiProvider)
.list(pageSize: 20, isRead: false);
if (!res.isOk || res.data == null || res.data!.list.isEmpty) return;
final notices = res.data!.list;
final latest = notices.fold<int>(0, (m, n) => n.id > m ? n.id : m);
final storage = ref.read(tokenStorageProvider);
// 首次运行只记录基线,避免把历史未读一次性弹成通知
final maxId = storage.noticeAlertMaxId;
if (maxId < 0) {
await storage.setNoticeAlertMaxId(latest);
return;
}
final fresh = notices.where((n) => n.id > maxId).toList()
..sort((a, b) => a.id.compareTo(b.id));
if (fresh.isEmpty) return;
// 前台时只更新基线(界面内已有红点/消息中心),退到后台才弹通知
final inBackground =
WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed;
if (inBackground) {
for (final notice in fresh) {
await LocalNotifications.instance.show(
id: notice.id,
title: notice.title,
body: notice.content,
route: noticeRoute(notice) ?? '/notice',
);
}
}
await storage.setNoticeAlertMaxId(latest);
} on ApiException {
// 网络波动忽略,下一轮再试
} finally {
_busy = false;
}
}
}
@@ -0,0 +1,18 @@
import '../data/notice_api.dart';
/// 消息可跳转的订单路由;无法识别业务类型时返回 null。
///
/// 与后端通知投递的 `bizType` 约定一致:
/// `survey_order` / `repair_dispatch` / `install_order` / `maintenance_order`。
String? noticeRoute(Notice notice) {
final id = notice.bizId;
if (id == null || id <= 0) return null;
final source = switch (notice.bizType) {
'survey_order' => 'survey',
'repair_dispatch' || 'repair_order' => 'repair',
'install_order' => 'install',
'maintenance_order' => 'maintenance',
_ => null,
};
return source == null ? null : '/order/$source/$id';
}