refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP

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

No files matched your search

+33 -9
View File
@@ -17,6 +17,9 @@ class SessionRestoreResult {
final UserDataBean? userData;
final UserPreviewDataBean? preview;
/// 恢复过程中 GetUserPreviewApi 的响应原文,供主页调试卡展示。
final String? rawPreviewResponse;
/// 服务器认为这次会话仍在登录中(`preview.isLogin`),或刚用令牌登录成功。
/// 只有为 `true` 时票据与风险页才开放写入,并且照常吃 60 秒登录后冷却。
final bool loggedIn;
@@ -25,6 +28,7 @@ class SessionRestoreResult {
required this.reusedCookie,
this.userData,
this.preview,
this.rawPreviewResponse,
this.loggedIn = false,
});
}
@@ -44,11 +48,22 @@ class SessionModel extends ChangeNotifier {
UserLoginResult? get gameLogin => _gameLogin;
String? get cookies => _cookies;
static String extractQRCode(String qrCodeToken) =>
ApiService.extractQRCode(qrCodeToken);
/// 构造会回写 Cookie 的服务。
///
/// 生产环境优先用会话里最新的 JSESSIONID:Title Server 可能在任意响应里
/// 轮换 Cookie,页面若一直拿着进入时的快照,后续请求就会带着旧值。
/// 直接构造页面的测试没有会话,才退回调用方传入的 [cookies]。
TitleApiService? createTitleService({String? cookies}) {
return TitleApiService.fromHolder(
cookies: _cookies ?? cookies,
onCookiesChanged: updateCookies,
);
}
Future<LoginResult> loginWithQr(String qrCodeToken) async {
final result = await _apiService.login(qrCodeToken);
/// [qrInput] 可以是 84 位二维码串,也可以是主页导出的二维码链接;
/// 格式不合预期时 [ApiService.login] 抛 [FormatException]。
Future<LoginResult> loginWithQr(String qrInput) async {
final result = await _apiService.login(qrInput);
if (result.success) {
_loginResult = result;
notifyListeners();
@@ -79,7 +94,11 @@ class SessionModel extends ChangeNotifier {
final config = TitleServerConfigHolder().config;
if (config == null) return;
final service = TitleApiService(config, cookies: cookies ?? _cookies);
final service = TitleApiService(
config,
cookies: cookies ?? _cookies,
onCookiesChanged: updateCookies,
);
try {
await service.userLogout(
userId: userId,
@@ -136,7 +155,10 @@ class SessionModel extends ChangeNotifier {
SessionShare share,
String cookie,
) async {
final service = TitleApiService.fromHolder(cookies: cookie);
final service = TitleApiService.fromHolder(
cookies: cookie,
onCookiesChanged: updateCookies,
);
if (service == null) return null;
try {
@@ -159,6 +181,7 @@ class SessionModel extends ChangeNotifier {
}
}
updateCookies(service.cookies);
final rawPreviewResponse = service.lastRawResponse;
// 服务器说这次会话还挂在机上,就按已登录处理:loginDateTime 取当下,
// 风险页与票据照常吃「登录后 60 秒冷却」,不用重新 UserLoginApi。
@@ -177,6 +200,7 @@ class SessionModel extends ChangeNotifier {
reusedCookie: true,
userData: userData,
preview: preview,
rawPreviewResponse: rawPreviewResponse,
loggedIn: _gameLogin != null,
);
} on TitleApiException {
@@ -185,9 +209,9 @@ class SessionModel extends ChangeNotifier {
}
void updateCookies(String? cookies) {
if (cookies != null && cookies.isNotEmpty) {
_cookies = cookies;
}
if (cookies == null || cookies.isEmpty || cookies == _cookies) return;
_cookies = cookies;
notifyListeners();
}
void reset() {
+10 -20
View File
@@ -1,3 +1,5 @@
import '../core/json_value.dart';
/// GetUserRatingApi 之外的角色(旅行伙伴)数据。
///
/// 对应客户端 `Net.VO.Mai2.UserCharacter`:只有 4 个字段,走
@@ -36,30 +38,18 @@ class UserCharacterBean {
useCount: useCount ?? this.useCount,
);
static int _toInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
factory UserCharacterBean.fromJson(Map<String, dynamic> json) =>
UserCharacterBean(
characterId: _toInt(json['characterId']),
level: _toInt(json['level']),
awakening: _toInt(json['awakening']),
useCount: _toInt(json['useCount']),
characterId: jsonInt(json['characterId']),
level: jsonInt(json['level']),
awakening: jsonInt(json['awakening']),
useCount: jsonInt(json['useCount']),
);
static List<UserCharacterBean> listFromResponse(Map<String, dynamic> json) {
final raw = json['userCharacterList'];
if (raw is! List) return const [];
return raw
.whereType<Map<String, dynamic>>()
.map(UserCharacterBean.fromJson)
.toList();
}
static List<UserCharacterBean> listFromResponse(Map<String, dynamic> json) =>
jsonMapList(json['userCharacterList'])
.map(UserCharacterBean.fromJson)
.toList();
}
/// `UserDetail.charaSlot` 固定 5 个槽,槽 0 是队长
+42 -58
View File
@@ -1,3 +1,5 @@
import '../core/json_value.dart';
class UserDataBean {
final int userId;
final String userName;
@@ -84,67 +86,49 @@ class UserDataBean {
});
factory UserDataBean.fromJson(Map<String, dynamic> json) {
final ud = (json['userData'] as Map<String, dynamic>?) ?? const {};
int toInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
bool toBool(dynamic v) {
if (v == null) return false;
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
return false;
}
String toStr(dynamic v) => v?.toString() ?? '';
final ud = jsonMap(json['userData']);
return UserDataBean(
userId: toInt(json['userId']),
userName: toStr(ud['userName']),
userId: jsonInt(json['userId']),
userName: jsonString(ud['userName']),
friendCode: ud['friendCode']?.toString(),
isNetMember: toBool(ud['isNetMember']),
point: toInt(ud['point']),
totalPoint: toInt(ud['totalPoint']),
playerRating: toInt(ud['playerRating']),
playerOldRating: toInt(ud['playerOldRating']),
playerNewRating: toInt(ud['playerNewRating']),
highestRating: toInt(ud['highestRating']),
musicRating: toInt(ud['musicRating']),
gradeRank: toInt(ud['gradeRank']),
courseRank: toInt(ud['courseRank']),
classRank: toInt(ud['classRank']),
nameplateId: toInt(ud['nameplateId']),
frameId: toInt(ud['frameId']),
iconId: toInt(ud['iconId']),
trophyId: toInt(ud['trophyId']),
plateId: toInt(ud['plateId']),
titleId: toInt(ud['titleId']),
partnerId: toInt(ud['partnerId']),
playCount: toInt(ud['playCount']),
currentPlayCount: toInt(ud['currentPlayCount']),
totalSync: toInt(ud['totalSync']),
totalAchievement: toInt(ud['totalAchievement']),
totalDeluxscore: toInt(ud['totalDeluxscore']),
totalAwake: toInt(ud['totalAwake']),
lastGameId: toStr(ud['lastGameId']),
lastRomVersion: toStr(ud['lastRomVersion']),
lastDataVersion: toStr(ud['lastDataVersion']),
lastLoginDate: toStr(ud['lastLoginDate']),
lastPlayDate: toStr(ud['lastPlayDate']),
firstGameId: toStr(ud['firstGameId']),
firstRomVersion: toStr(ud['firstRomVersion']),
firstDataVersion: toStr(ud['firstDataVersion']),
firstPlayDate: toStr(ud['firstPlayDate']),
lastRegionName: toStr(ud['lastRegionName']),
lastRegionId: toInt(ud['lastRegionId']),
dailyBonusDate: toStr(ud['dailyBonusDate']),
banState: toInt(json['banState']),
isNetMember: jsonBool(ud['isNetMember']),
point: jsonInt(ud['point']),
totalPoint: jsonInt(ud['totalPoint']),
playerRating: jsonInt(ud['playerRating']),
playerOldRating: jsonInt(ud['playerOldRating']),
playerNewRating: jsonInt(ud['playerNewRating']),
highestRating: jsonInt(ud['highestRating']),
musicRating: jsonInt(ud['musicRating']),
gradeRank: jsonInt(ud['gradeRank']),
courseRank: jsonInt(ud['courseRank']),
classRank: jsonInt(ud['classRank']),
nameplateId: jsonInt(ud['nameplateId']),
frameId: jsonInt(ud['frameId']),
iconId: jsonInt(ud['iconId']),
trophyId: jsonInt(ud['trophyId']),
plateId: jsonInt(ud['plateId']),
titleId: jsonInt(ud['titleId']),
partnerId: jsonInt(ud['partnerId']),
playCount: jsonInt(ud['playCount']),
currentPlayCount: jsonInt(ud['currentPlayCount']),
totalSync: jsonInt(ud['totalSync']),
totalAchievement: jsonInt(ud['totalAchievement']),
totalDeluxscore: jsonInt(ud['totalDeluxscore']),
totalAwake: jsonInt(ud['totalAwake']),
lastGameId: jsonString(ud['lastGameId']),
lastRomVersion: jsonString(ud['lastRomVersion']),
lastDataVersion: jsonString(ud['lastDataVersion']),
lastLoginDate: jsonString(ud['lastLoginDate']),
lastPlayDate: jsonString(ud['lastPlayDate']),
firstGameId: jsonString(ud['firstGameId']),
firstRomVersion: jsonString(ud['firstRomVersion']),
firstDataVersion: jsonString(ud['firstDataVersion']),
firstPlayDate: jsonString(ud['firstPlayDate']),
lastRegionName: jsonString(ud['lastRegionName']),
lastRegionId: jsonInt(ud['lastRegionId']),
dailyBonusDate: jsonString(ud['dailyBonusDate']),
banState: jsonInt(json['banState']),
);
}
}
+20 -37
View File
@@ -1,3 +1,5 @@
import '../core/json_value.dart';
/// `Net.VO.Mai2.UserKaleidxScope`(万花筒 / 宿命之门),走
/// `upsertUserAll.userKaleidxScopeList`,主键是 `gateId`。
///
@@ -130,47 +132,28 @@ class UserKaleidxScopeBean {
: ''),
);
static int _toInt(dynamic v) {
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
static bool _toBool(dynamic v) {
if (v is bool) return v;
if (v is num) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
return false;
}
factory UserKaleidxScopeBean.fromJson(Map<String, dynamic> json) =>
UserKaleidxScopeBean(
gateId: _toInt(json['gateId']),
isGateFound: _toBool(json['isGateFound']),
isKeyFound: _toBool(json['isKeyFound']),
isClear: _toBool(json['isClear']),
totalRestLife: _toInt(json['totalRestLife']),
totalAchievement: _toInt(json['totalAchievement']),
totalDeluxscore: _toInt(json['totalDeluxscore']),
bestAchievement: _toInt(json['bestAchievement']),
bestDeluxscore: _toInt(json['bestDeluxscore']),
bestAchievementDate: json['bestAchievementDate']?.toString() ?? '',
bestDeluxscoreDate: json['bestDeluxscoreDate']?.toString() ?? '',
playCount: _toInt(json['playCount']),
clearDate: json['clearDate']?.toString() ?? '',
lastPlayDate: json['lastPlayDate']?.toString() ?? '',
isInfoWatched: _toBool(json['isInfoWatched']),
gateId: jsonInt(json['gateId']),
isGateFound: jsonBool(json['isGateFound']),
isKeyFound: jsonBool(json['isKeyFound']),
isClear: jsonBool(json['isClear']),
totalRestLife: jsonInt(json['totalRestLife']),
totalAchievement: jsonInt(json['totalAchievement']),
totalDeluxscore: jsonInt(json['totalDeluxscore']),
bestAchievement: jsonInt(json['bestAchievement']),
bestDeluxscore: jsonInt(json['bestDeluxscore']),
bestAchievementDate: jsonString(json['bestAchievementDate']),
bestDeluxscoreDate: jsonString(json['bestDeluxscoreDate']),
playCount: jsonInt(json['playCount']),
clearDate: jsonString(json['clearDate']),
lastPlayDate: jsonString(json['lastPlayDate']),
isInfoWatched: jsonBool(json['isInfoWatched']),
);
static List<UserKaleidxScopeBean> listFromResponse(
Map<String, dynamic> json,
) {
final raw = json['userKaleidxScopeList'];
if (raw is! List) return const [];
return raw
.whereType<Map<String, dynamic>>()
.map(UserKaleidxScopeBean.fromJson)
.toList();
}
) => jsonMapList(json['userKaleidxScopeList'])
.map(UserKaleidxScopeBean.fromJson)
.toList();
}
+23 -41
View File
@@ -1,3 +1,5 @@
import '../core/json_value.dart';
class UserPreviewDataBean {
final int userId;
final String userName;
@@ -49,48 +51,28 @@ class UserPreviewDataBean {
});
factory UserPreviewDataBean.fromJson(Map<String, dynamic> json) {
int toInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is String) return int.tryParse(v) ?? 0;
if (v is double) return v.toInt();
return 0;
}
bool toBool(dynamic v) {
if (v == null) return false;
if (v is bool) return v;
if (v is int) return v != 0;
if (v is String) return v == '1' || v.toLowerCase() == 'true';
return false;
}
String toStr(dynamic v) => v?.toString() ?? '';
return UserPreviewDataBean(
userId: toInt(json['userId']),
userName: toStr(json['userName']),
isLogin: toBool(json['isLogin']),
loginId: toInt(json['loginId']),
lastGameId: toStr(json['lastGameId']),
lastRomVersion: toStr(json['lastRomVersion']),
lastDataVersion: toStr(json['lastDataVersion']),
lastLoginDate: toStr(json['lastLoginDate']),
lastPlayDate: toStr(json['lastPlayDate']),
playerRating: toInt(json['playerRating']),
nameplateId: toInt(json['nameplateId']),
iconId: toInt(json['iconId']),
trophyId: toInt(json['trophyId']),
isNetMember: toBool(json['isNetMember']),
isInherit: toBool(json['isInherit']),
totalAwake: toInt(json['totalAwake']),
dispRate: toInt(json['dispRate']),
dailyBonusDate: toStr(json['dailyBonusDate']),
headPhoneVolume: (json['headPhoneVolume'] is String)
? (int.tryParse(json['headPhoneVolume']) ?? 0)
: toInt(json['headPhoneVolume']),
banState: toInt(json['banState']),
errorId: toInt(json['errorId']),
userId: jsonInt(json['userId']),
userName: jsonString(json['userName']),
isLogin: jsonBool(json['isLogin']),
loginId: jsonInt(json['loginId']),
lastGameId: jsonString(json['lastGameId']),
lastRomVersion: jsonString(json['lastRomVersion']),
lastDataVersion: jsonString(json['lastDataVersion']),
lastLoginDate: jsonString(json['lastLoginDate']),
lastPlayDate: jsonString(json['lastPlayDate']),
playerRating: jsonInt(json['playerRating']),
nameplateId: jsonInt(json['nameplateId']),
iconId: jsonInt(json['iconId']),
trophyId: jsonInt(json['trophyId']),
isNetMember: jsonBool(json['isNetMember']),
isInherit: jsonBool(json['isInherit']),
totalAwake: jsonInt(json['totalAwake']),
dispRate: jsonInt(json['dispRate']),
dailyBonusDate: jsonString(json['dailyBonusDate']),
headPhoneVolume: jsonInt(json['headPhoneVolume']),
banState: jsonInt(json['banState']),
errorId: jsonInt(json['errorId']),
);
}
}
+12 -32
View File
@@ -1,3 +1,5 @@
import '../core/json_value.dart';
/// GetUserRatingApi 的 `userRating` 结构,移植自 Empurple 的 `UserRatingData`。
///
/// 服务端已按 RA 排好 BEST 35 / BEST 15,客户端不再重排。
@@ -43,21 +45,13 @@ class UserRatingItemBean {
MusicLevel get musicLevel => MusicLevel.fromInt(level);
factory UserRatingItemBean.fromJson(Map<String, dynamic> json) {
int toInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
return UserRatingItemBean(
musicId: toInt(json['musicId']),
level: toInt(json['level']),
achievement: toInt(json['achievement']),
comboStatus: toInt(json['comboStatus']),
syncStatus: toInt(json['syncStatus']),
romVersion: toInt(json['romVersion']),
musicId: jsonInt(json['musicId']),
level: jsonInt(json['level']),
achievement: jsonInt(json['achievement']),
comboStatus: jsonInt(json['comboStatus']),
syncStatus: jsonInt(json['syncStatus']),
romVersion: jsonInt(json['romVersion']),
);
}
}
@@ -73,28 +67,14 @@ class UserRatingBean {
required this.newRatingList,
});
static List<UserRatingItemBean> _parseList(dynamic raw) {
if (raw is! List) return const [];
return raw
.whereType<Map<String, dynamic>>()
.map(UserRatingItemBean.fromJson)
.toList();
}
static List<UserRatingItemBean> _parseList(dynamic raw) =>
jsonMapList(raw).map(UserRatingItemBean.fromJson).toList();
factory UserRatingBean.fromJson(Map<String, dynamic> json) {
int toInt(dynamic v) {
if (v == null) return 0;
if (v is int) return v;
if (v is num) return v.toInt();
if (v is String) return int.tryParse(v) ?? 0;
return 0;
}
final userRating =
(json['userRating'] as Map<String, dynamic>?) ?? const {};
final userRating = jsonMap(json['userRating']);
return UserRatingBean(
userId: toInt(json['userId']),
userId: jsonInt(json['userId']),
ratingList: _parseList(userRating['ratingList']),
newRatingList: _parseList(userRating['newRatingList']),
);