fix:参考金值,研听,研值工程通用通用工程公共能力,重点对比网络请求、登录认证、状态管理、本地存储、WebView、推送、版本更新、一键登录、主题/UI 等模块,确定后续统一整改方案
This commit is contained in:
1 parent
6997a65c96
commit
d364c6d908
46 files changed
+2060
-395
No files matched your search
@@ -1,33 +1,177 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
enum DeepLinkSource { push, appLink, universalLink, customScheme }
|
||||
|
||||
import 'app_router.dart';
|
||||
typedef DeepLinkNavigator = bool Function(String location);
|
||||
typedef DeepLinkAuthGuard = bool Function(String location);
|
||||
|
||||
/// 统一将 Scheme、Universal Link 和推送 payload 收敛为应用内路由。
|
||||
class DeepLinkEvent {
|
||||
const DeepLinkEvent({
|
||||
required this.raw,
|
||||
required this.source,
|
||||
required this.receivedAt,
|
||||
this.eventId,
|
||||
});
|
||||
|
||||
final String raw;
|
||||
final DeepLinkSource source;
|
||||
final DateTime receivedAt;
|
||||
final String? eventId;
|
||||
}
|
||||
|
||||
/// Source adapters feed one parser and a bounded FIFO. Navigation starts only
|
||||
/// after GoRouter has a context and the auth guard accepts the target.
|
||||
abstract final class DeepLinkService {
|
||||
static void handle(String raw) {
|
||||
final target = _route(raw);
|
||||
if (target != null) rootNavigatorKey.currentContext?.go(target);
|
||||
static const maxPending = 20;
|
||||
static const expiry = Duration(minutes: 15);
|
||||
static const _environmentHosts = String.fromEnvironment(
|
||||
'JINZHI_DEEP_LINK_HOSTS',
|
||||
);
|
||||
|
||||
static final List<DeepLinkEvent> _pending = [];
|
||||
static final Set<String> _pendingEventIds = {};
|
||||
static final Map<String, DateTime> _seenEventIds = {};
|
||||
static final Set<String> _configuredHttpsHosts = _normalizedHosts(
|
||||
_environmentHosts.split(','),
|
||||
);
|
||||
static Set<String> _trustedHttpsHosts = {..._configuredHttpsHosts};
|
||||
static DeepLinkNavigator? _navigator;
|
||||
static DeepLinkAuthGuard _authGuard = (_) => true;
|
||||
|
||||
static int get pendingCount => _pending.length;
|
||||
|
||||
static void handle(String raw) => enqueue(raw, source: DeepLinkSource.push);
|
||||
|
||||
static void handleUri(Uri uri) => enqueue(
|
||||
uri.toString(),
|
||||
source: uri.scheme.toLowerCase() == 'jinzhi'
|
||||
? DeepLinkSource.customScheme
|
||||
: DeepLinkSource.universalLink,
|
||||
);
|
||||
|
||||
static void enqueue(
|
||||
String raw, {
|
||||
required DeepLinkSource source,
|
||||
DateTime? receivedAt,
|
||||
}) {
|
||||
final normalized = raw.trim();
|
||||
if (normalized.isEmpty) return;
|
||||
final event = DeepLinkEvent(
|
||||
raw: normalized,
|
||||
source: source,
|
||||
receivedAt: receivedAt ?? DateTime.now(),
|
||||
eventId: _eventId(normalized),
|
||||
);
|
||||
_discardExpired(event.receivedAt);
|
||||
_seenEventIds.removeWhere(
|
||||
(_, seenAt) => event.receivedAt.difference(seenAt) > expiry,
|
||||
);
|
||||
if (event.eventId case final id? when _seenEventIds.containsKey(id)) return;
|
||||
if (_pending.length == maxPending) _removeAt(0);
|
||||
_pending.add(event);
|
||||
if (event.eventId case final id?) {
|
||||
_pendingEventIds.add(id);
|
||||
_seenEventIds[id] = event.receivedAt;
|
||||
}
|
||||
_drain();
|
||||
}
|
||||
|
||||
static void attachRouter(
|
||||
DeepLinkNavigator navigator, {
|
||||
DeepLinkAuthGuard? authGuard,
|
||||
}) {
|
||||
_navigator = navigator;
|
||||
_authGuard = authGuard ?? (_) => true;
|
||||
_drain();
|
||||
}
|
||||
|
||||
static void notifyAuthChanged() => _drain();
|
||||
|
||||
static void detachRouter() => _navigator = null;
|
||||
|
||||
/// Universal/App Link domains are supplied by the release environment. An
|
||||
/// empty allowlist intentionally disables HTTPS deep links until the native
|
||||
/// association domain is confirmed.
|
||||
static void configureTrustedHttpsHosts(Iterable<String> hosts) {
|
||||
_trustedHttpsHosts = _normalizedHosts(hosts);
|
||||
}
|
||||
|
||||
static void resetForTest() {
|
||||
_pending.clear();
|
||||
_pendingEventIds.clear();
|
||||
_seenEventIds.clear();
|
||||
_trustedHttpsHosts = {..._configuredHttpsHosts};
|
||||
_navigator = null;
|
||||
_authGuard = (_) => true;
|
||||
}
|
||||
|
||||
static void _drain() {
|
||||
final navigator = _navigator;
|
||||
if (navigator == null) return;
|
||||
_discardExpired(DateTime.now());
|
||||
while (_pending.isNotEmpty) {
|
||||
final event = _pending.first;
|
||||
final target = _route(event.raw);
|
||||
if (target == null) {
|
||||
_removeAt(0);
|
||||
continue;
|
||||
}
|
||||
if (!_authGuard(target) || !navigator(target)) return;
|
||||
_removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void _discardExpired(DateTime now) {
|
||||
while (_pending.isNotEmpty &&
|
||||
now.difference(_pending.first.receivedAt) > expiry) {
|
||||
_removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void _removeAt(int index) {
|
||||
final removed = _pending.removeAt(index);
|
||||
if (removed.eventId case final id?) _pendingEventIds.remove(id);
|
||||
}
|
||||
|
||||
static String? _eventId(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map) {
|
||||
final id = '${decoded['event_id'] ?? decoded['eventId'] ?? ''}'.trim();
|
||||
if (id.isNotEmpty) return id;
|
||||
}
|
||||
} catch (_) {}
|
||||
final uri = Uri.tryParse(raw);
|
||||
final id = uri?.queryParameters['event_id']?.trim();
|
||||
return id == null || id.isEmpty ? null : id;
|
||||
}
|
||||
|
||||
static String? _route(String raw) {
|
||||
var source = raw.trim();
|
||||
try {
|
||||
final json = jsonDecode(source);
|
||||
if (json is Map) {
|
||||
source = '${json['deep_link'] ?? json['url'] ?? json['link'] ?? ''}';
|
||||
final decoded = jsonDecode(source);
|
||||
if (decoded is Map) {
|
||||
source =
|
||||
'${decoded['deep_link'] ?? decoded['url'] ?? decoded['link'] ?? ''}';
|
||||
}
|
||||
} catch (_) {}
|
||||
final uri = Uri.tryParse(source);
|
||||
if (uri == null) return null;
|
||||
if (uri.scheme == 'jinzhi') {
|
||||
final path = uri.path.isEmpty ? '/assets' : uri.path;
|
||||
if (uri.scheme.toLowerCase() == 'jinzhi') {
|
||||
final path = uri.path.isNotEmpty
|
||||
? uri.path
|
||||
: uri.host.isNotEmpty
|
||||
? '/${uri.host}'
|
||||
: '/assets';
|
||||
return _allowed(path)
|
||||
? '$path${uri.hasQuery ? '?${uri.query}' : ''}'
|
||||
: null;
|
||||
}
|
||||
if ((uri.scheme == 'https' || uri.scheme == 'http') && _allowed(uri.path)) {
|
||||
if (uri.scheme.toLowerCase() == 'https' &&
|
||||
uri.port == 443 &&
|
||||
uri.userInfo.isEmpty &&
|
||||
_trustedHttpsHosts.contains(uri.host.toLowerCase()) &&
|
||||
_allowed(uri.path)) {
|
||||
return '${uri.path}${uri.hasQuery ? '?${uri.query}' : ''}';
|
||||
}
|
||||
return null;
|
||||
@@ -40,4 +184,9 @@ abstract final class DeepLinkService {
|
||||
'/profile',
|
||||
'/settings',
|
||||
}.contains(path);
|
||||
|
||||
static Set<String> _normalizedHosts(Iterable<String> hosts) => {
|
||||
for (final host in hosts)
|
||||
if (host.trim().isNotEmpty) host.trim().toLowerCase(),
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import '../../features/web/presentation/mall_web_host.dart';
|
||||
import '../theme/app_icons.dart';
|
||||
import '../widgets/app_toast.dart';
|
||||
import 'app_router.dart';
|
||||
import '../storage/storage_service.dart';
|
||||
import '../storage/preferences_storage.dart';
|
||||
|
||||
class TabShell extends ConsumerStatefulWidget {
|
||||
const TabShell({
|
||||
@@ -65,7 +65,9 @@ class _TabShellState extends ConsumerState<TabShell>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mallEntryPromoStore = MallEntryPromoStore(StorageService.to.preferences);
|
||||
_mallEntryPromoStore = MallEntryPromoStore(
|
||||
ref.read(preferencesStorageProvider),
|
||||
);
|
||||
_mallEntryTipConsumed = _mallEntryPromoStore.hasOpenedMall;
|
||||
_mallHomeBadgeSeen = _mallEntryPromoStore.hasSeenHomeBadge;
|
||||
MallWebHostController.instance.state.addListener(_syncMallHost);
|
||||
|
||||
Reference in new issue
Block a user