Files
Ichino/lib/services/account_backup_service.dart
T
Bad0RANG3 e9357f5969 refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析
- 主页与音乐风险页拆成组件,风险功能共用一套运行框架
- 修复设置页作为主页页签保存时弹掉根路由导致的黑屏
- 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正,
  补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对
  BoringSSL 的 TLS 指纹拦截(证书校验保持开启)
- 补开发预览数据与测试
2026-10-25 15:51:10 +08:00

118 lines
3.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'title_api_service.dart';
/// 一次账号备份的结果。
class AccountBackup {
static const int formatVersion = 1;
final Map<String, dynamic> document;
/// 读失败的段名(含原因)。备份不因为单段失败而整体作废——
/// 私服不一定实现了所有 GetUser* 接口。
final List<String> failedSections;
const AccountBackup({required this.document, required this.failedSections});
/// 备份正文就是这一长串 JSON。
String encode() => const JsonEncoder.withIndent(' ').convert(document);
int get byteLength => utf8.encode(encode()).length;
bool get isComplete => failedSections.isEmpty;
}
/// 把已经读到的各段数据拼成备份文档。与取数分开,方便单测。
Map<String, dynamic> buildBackupDocument({
required int userId,
required Map<String, Object?> sections,
DateTime? exportedAt,
}) {
return {
'backupFormat': AccountBackup.formatVersion,
'exportedAt': (exportedAt ?? DateTime.now()).toUtc().toIso8601String(),
'userId': userId,
'data': sections,
};
}
/// 备份要读的道具类别,对应 `Net.VO.Mai2.ItemKind`。
///
/// 只列客户端下行真会读的那些:`StateUserDownload.cs:49-52` 逐个 kind 拉
/// `GetUserItemApi`,而 itemKind 15(KaleidxScopeKey)客户端从不读
/// (钥匙是 `userKaleidxScopeList[].isKeyFound`),所以不在此列。
const List<int> kBackupItemKinds = [1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12];
/// 读取用户数据并汇总成备份。
///
/// 每段独立容错:某私服没实现某个接口时,其余部分照样能备出来,
/// 失败原因记在 [AccountBackup.failedSections] 里给界面显示。
class AccountBackupService {
final TitleApiService _api;
final int userId;
const AccountBackupService(this._api, {required this.userId});
/// [onSection] 每开始读一段回调一次,供界面显示进度。
Future<AccountBackup> run({void Function(String section)? onSection}) async {
final sections = <String, Object?>{};
final failed = <String>[];
Future<void> gather(String name, Future<Object?> Function() read) async {
onSection?.call(name);
try {
final value = await read();
sections[name] = value;
} catch (e) {
failed.add('$name: ${describeApiError(e)}');
}
}
// 单段一个请求:并发跑,且一段失败不牵连其他段。
await Future.wait([
gather('userData', () => _api.getUserData(userId)),
gather('userExtend', () => _api.getUserExtend(userId)),
gather('userOption', () => _api.getUserOption(userId)),
gather('userRating', () => _api.getUserRating(userId)),
gather('userCharge', () => _api.getUserCharge(userId)),
gather('userActivity', () => _api.getUserActivity(userId)),
gather('userMissionData', () => _api.getUserMissionData(userId)),
gather(
'userCharacterList',
() async => [
for (final c in await _api.getUserCharacters(userId)) c.toWireJson(),
],
),
gather('userMapList', () => _api.getUserMaps(userId)),
gather(
'userKaleidxScopeList',
() async => [
for (final s in await _api.getUserKaleidxScopes(userId))
s.toWireJson(),
],
),
]);
// userItemList 是按 itemKind 分页拉的,按 kind 分组存放,
// 读回时一眼能看出哪类是空的、哪类根本没读到。
final items = <String, Object?>{};
await Future.wait([
for (final kind in kBackupItemKinds)
() async {
onSection?.call('itemKind $kind');
try {
items['$kind'] = await _api.getUserItems(userId, kind);
} catch (e) {
failed.add('userItemList/$kind: ${describeApiError(e)}');
}
}(),
]);
sections['userItemList'] = items;
return AccountBackup(
document: buildBackupDocument(userId: userId, sections: sections),
failedSections: failed,
);
}
}