fix:参考金值,研听,研值工程通用通用工程公共能力,重点对比网络请求、登录认证、状态管理、本地存储、WebView、推送、版本更新、一键登录、主题/UI 等模块,确定后续统一整改方案

This commit is contained in:
jingyun committed 2026-09-10 09:52:36 +08:00
1 parent 6997a65c96
commit d364c6d908
46 files changed
+2060 -395

No files matched your search

+161 -12
View File
@@ -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(),
};
}
+4 -2
View File
@@ -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);