refactor: 拆分主页与公共组件,扫码登录改走 WinHTTP
- 抽出 lib/core 的日期/JSON 工具,收敛各页重复解析 - 主页与音乐风险页拆成组件,风险功能共用一套运行框架 - 修复设置页作为主页页签保存时弹掉根路由导致的黑屏 - 登录对齐 Astral aimedb 协议:84 位二维码校验、签名与请求头修正, 补 GlobalSign 中间证书;Windows 改用 WinHTTP 绕过服务端对 BoringSSL 的 TLS 指纹拦截(证书校验保持开启) - 补开发预览数据与测试
This commit is contained in:
1 parent
0cb5320290
commit
e9357f5969
55 files changed
+4387
-3392
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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']),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user