Files
Ichino/lib/services/account_backup_service.dart
T
Cookiues 0cb5320290 feat: 主页账号备份,并修好 B50 预览缩不小
账号备份(AccountBackupCard + AccountBackupService):
- 并发读 GetUserData/Extend/Option/Rating/Charge/Activity/MissionData +
  Character + Map(分页)+ KaleidxScope + 各 itemKind 的 GetUserItemApi,
  汇成一个 JSON 字符串可复制到剪贴板。舞里程与总游玩数据本就在
  userData 的 point/totalPoint/playCount 里。
- itemKind 15(KaleidxScopeKey)不列进回读清单:客户端 ExportUserItems
  从不输出它,下行也没有读它的路径,钥匙实际是
  userKaleidxScopeList[].isKeyFound。
- 全程只发 GetUser*Api,不碰 UpsertUserAllApi。
- 逐段容错:私服没实现某个接口时其余照样备出,失败段单独列出——
  「读到但是空的」和「没读到」在 userItemList 里按 kind 分开存,可区分。
- 取数与拼装分开,buildBackupDocument/encode 部分是纯函数,单测覆盖。

B50 预览:原因是 InteractiveViewer(constrained:false) 给孩子无界约束,
FittedBox(fit: fitWidth) 在无界下根本不缩放,海报按 1762x1850 原尺寸铺开;
而 minScale 又算成 1.0,于是既巨大又缩不动。改为给一个 contain 尺寸的
有限盒子让 FittedBox 真正缩放,minScale 固定 1.0(这个版本没有
initialScale 可用)。RepaintBoundary 留在 FittedBox 内侧,绘制尺寸仍是
1762x1850,导出分辨率不受影响,并加了断言守住这点。
2026-10-02 12:26:03 +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: $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: $e');
}
}(),
]);
sections['userItemList'] = items;
return AccountBackup(
document: buildBackupDocument(userId: userId, sections: sections),
failedSections: failed,
);
}
}