This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import 'package:shared_preferences/shared_preferences.dart';
/// 本地轻量配置存储(SharedPreferences)
/// device_id 为游客计费与风控的设备指纹:
/// 生产要求 Android 存 Keystore 加密的 SharedPreferences、iOS 存 Keychain(卸载重装保留),
/// 骨架阶段先持久化到 SharedPreferences,接入原生通道时替换此处实现即可。
class LocalStore {
LocalStore._(this._prefs);
final SharedPreferences _prefs;
static const String _keyDeviceId = 'device_id';
static const String _keyCachedDomain = 'cached_domain';
static const String _keyPendingInviteCode = 'pending_invite_code';
static const String _keyInviteMatchReported = 'invite_match_reported';
static LocalStore? _instance;
/// 初始化(main 中 await 完成后再 runApp)
static Future<LocalStore> init() async {
final prefs = await SharedPreferences.getInstance();
_instance = LocalStore._(prefs);
return _instance!;
}
static LocalStore get I => _instance!;
/// 读取设备 ID;不存在时生成 UUID v4 并持久化
String ensureDeviceId(String Function() uuidV4) {
final existing = _prefs.getString(_keyDeviceId);
if (existing != null && existing.isNotEmpty) return existing;
final id = uuidV4();
_prefs.setString(_keyDeviceId, id);
return id;
}
String get deviceId => _prefs.getString(_keyDeviceId) ?? '';
/// 待绑定邀请码(首启口令解析/模糊归因命中后写入,注册成功或手填后清除)
String? get pendingInviteCode => _prefs.getString(_keyPendingInviteCode);
Future<void> savePendingInviteCode(String code) =>
_prefs.setString(_keyPendingInviteCode, code);
Future<void> clearPendingInviteCode() => _prefs.remove(_keyPendingInviteCode);
/// 模糊归因是否已上报过(每安装仅一次,避免重复请求)
bool get inviteMatchReported => _prefs.getBool(_keyInviteMatchReported) ?? false;
Future<void> markInviteMatchReported() =>
_prefs.setBool(_keyInviteMatchReported, true);
/// API 域名缓存(含 https:// 前缀的 Base URL)
String? get cachedBaseUrl => _prefs.getString(_keyCachedDomain);
Future<void> saveBaseUrl(String baseUrl) =>
_prefs.setString(_keyCachedDomain, baseUrl);
}
@@ -0,0 +1,50 @@
import 'package:shared_preferences/shared_preferences.dart';
/// 搜索历史本地存储(《01-开发文档》2.5.8:App 端本地存储最近 10 条,不上报服务端)
/// 最新搜索排最前,重复关键词去重置顶,超出 10 条截断。
class SearchHistoryStore {
SearchHistoryStore._(this._prefs);
final SharedPreferences _prefs;
static const String _key = 'search_history';
static const int maxCount = 10;
static SearchHistoryStore? _instance;
/// 初始化(main 阶段调用)
static Future<SearchHistoryStore> init() async {
final prefs = await SharedPreferences.getInstance();
_instance = SearchHistoryStore._(prefs);
return _instance!;
}
/// 测试注入
static void initForTest(SharedPreferences prefs) {
_instance = SearchHistoryStore._(prefs);
}
static SearchHistoryStore get I => _instance!;
/// 历史列表(最新在前)
List<String> get history => _prefs.getStringList(_key) ?? const [];
/// 记录一次搜索:去重置顶,保留最近 [maxCount] 条
Future<void> add(String keyword) async {
final word = keyword.trim();
if (word.isEmpty) return;
final list = [...history]..remove(word);
list.insert(0, word);
if (list.length > maxCount) list.length = maxCount;
await _prefs.setStringList(_key, list);
}
/// 删除单条
Future<void> remove(String keyword) async {
final list = [...history]..remove(keyword);
await _prefs.setStringList(_key, list);
}
/// 清空
Future<void> clear() => _prefs.remove(_key);
}
+44
View File
@@ -0,0 +1,44 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Token 安全存储(02-技术文档:flutter_secure_storage)
/// 仅存 access / refresh,业务代码不落明文日志。
class TokenStore {
TokenStore._();
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static const String _keyAccess = 'access_token';
static const String _keyRefresh = 'refresh_token';
/// 内存缓存:Dio 拦截器同步读取,避免每请求异步取 IO
static String? accessToken;
static String? refreshToken;
static Future<void> save({
required String access,
required String refresh,
}) async {
accessToken = access;
refreshToken = refresh;
await _storage.write(key: _keyAccess, value: access);
await _storage.write(key: _keyRefresh, value: refresh);
}
/// 启动时恢复登录态
static Future<void> restore() async {
accessToken = await _storage.read(key: _keyAccess);
refreshToken = await _storage.read(key: _keyRefresh);
}
/// 登出 / Token 失效:清空内存与本地
static Future<void> clear() async {
accessToken = null;
refreshToken = null;
await _storage.delete(key: _keyAccess);
await _storage.delete(key: _keyRefresh);
}
static bool get hasToken => accessToken != null && accessToken!.isNotEmpty;
}
@@ -0,0 +1,78 @@
import 'package:shared_preferences/shared_preferences.dart';
/// 观看上报去重存储(《03-接口文档》5.3:客户端本地按 video_id+日期 去重,
/// 同日同视频只上报一次)。游客免费额度按设备计费,杀进程重进若重复上报会
/// 白扣游客当日次数,因此必须持久化而非仅内存。
///
/// key 按日分组:`watch_yyyyMMdd` / `complete_yyyyMMdd`,值为当日已上报的
/// video_id 列表;每次访问时惰性清理非今日 key。
class WatchDedupStore {
WatchDedupStore._(this._prefs);
final SharedPreferences _prefs;
static const String _watchPrefix = 'watch_';
static const String _completePrefix = 'complete_';
static WatchDedupStore? _instance;
/// 初始化(splash 阶段调用)
static Future<WatchDedupStore> init() async {
final prefs = await SharedPreferences.getInstance();
_instance = WatchDedupStore._(prefs);
return _instance!;
}
/// 测试注入
static void initForTest(SharedPreferences prefs) {
_instance = WatchDedupStore._(prefs);
}
static WatchDedupStore get I => _instance!;
String get _today {
final now = DateTime.now();
return '${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}';
}
/// 是否已上报过 watch
bool hasWatched(int videoId) => _contains(_watchPrefix, videoId);
/// 标记已上报 watch
Future<void> markWatched(int videoId) => _mark(_watchPrefix, videoId);
/// 是否已上报过 complete
bool hasCompleted(int videoId) => _contains(_completePrefix, videoId);
/// 标记已上报 complete
Future<void> markCompleted(int videoId) => _mark(_completePrefix, videoId);
bool _contains(String prefix, int videoId) {
_cleanup(prefix);
final list = _prefs.getStringList('$prefix$_today') ?? const [];
return list.contains(videoId.toString());
}
Future<void> _mark(String prefix, int videoId) async {
_cleanup(prefix);
final key = '$prefix$_today';
final list = _prefs.getStringList(key) ?? <String>[];
final id = videoId.toString();
if (!list.contains(id)) {
list.add(id);
await _prefs.setStringList(key, list);
}
}
/// 惰性清理:删除该前缀下所有非今日的 key
void _cleanup(String prefix) {
final todayKey = '$prefix$_today';
final stale = _prefs
.getKeys()
.where((k) => k.startsWith(prefix) && k != todayKey)
.toList();
for (final k in stale) {
_prefs.remove(k);
}
}
}