refactor: 抽取高危功能页共享脚手架与传输层日志
诊断:功能票 / 解锁歌曲 / 收藏品 / 旅行伙伴 / 数值修改 / 一键跑图 / 万花筒 / 高危 hub 这些页面各自抄了一份同样的脚手架——冷却计时器、_Step 枚举、自动退登开关、卡片/提示/进度卡;TitleApiService.getUserPreview 还 重复实现了整套传输层;服务层有 37 处裸 print 会把 token / JSESSIONID 打进日志。 - 新增共享原语,7 个功能页切换到: * widgets/cooldown_mixin.dart CooldownMixin(登录后冷却倒计时) * widgets/step_progress.dart RiskStep + StepProgressCard * widgets/app_card.dart AppCard / SectionTitle / AutoLogoutToggle * widgets/app_notice.dart AppNotice + context.showSnack - TitleApiService: getUserPreview 收敛进通用 _callApi;新增 fromHolder() 工厂与 config getter,消除散落的 isConfigured + config! - 传输层 print -> ApiLog(仅 debug 输出),删除全部 avoid_print 忽略 - UserAllPayloadBuilder 新增 placeholderMusicData()(原 5 处重复) - LoginPage 从 main.dart 抽到 pages/login_page.dart,入口只留 main + MyApp - 更新 README / CLAUDE.md 目录结构与已过时的 web/依赖说明 验证:flutter analyze 0 issue(含 info);flutter test 96/96 通过。 纯结构重构,行为保持不变;仅有意统一了功能票 / 音乐风险 / hub 的 卡片与步骤视觉样式。 净删除约 2176 行重复代码。
This commit is contained in:
1 parent
0808e367f7
commit
45214b11a0
21 files changed
+1196
-2176
No files matched your search
@@ -9,6 +9,7 @@ import 'package:pointycastle/export.dart';
|
||||
|
||||
import '../config/title_server_config.dart';
|
||||
import '../models/user_data.dart';
|
||||
import 'api_log.dart';
|
||||
import '../models/user_character.dart';
|
||||
import '../models/user_kaleidx_scope.dart';
|
||||
import '../models/user_preview.dart';
|
||||
@@ -45,8 +46,21 @@ class TitleApiService {
|
||||
|
||||
TitleApiService(this._config, {this._cookies});
|
||||
|
||||
/// 从 [TitleServerConfigHolder] 构造;未配置时返回 `null`。
|
||||
///
|
||||
/// 各页原本都写 `if (!holder.isConfigured) ...; TitleApiService(holder.config!, ...)`,
|
||||
/// 收口成一个工厂,避免 `config!` 与 `isConfigured` 散落在十几个地方。
|
||||
static TitleApiService? fromHolder({String? cookies}) {
|
||||
final holder = TitleServerConfigHolder();
|
||||
if (!holder.isConfigured) return null;
|
||||
return TitleApiService(holder.config!, cookies: cookies);
|
||||
}
|
||||
|
||||
String? get cookies => _cookies;
|
||||
|
||||
/// 组装 payload 时需要同一份配置(地点 / 区域 / clientId)。
|
||||
TitleServerConfig get config => _config;
|
||||
|
||||
Uint8List _aesEncrypt(Uint8List plaintext) {
|
||||
final cipher = PaddedBlockCipherImpl(
|
||||
PKCS7Padding(),
|
||||
@@ -107,8 +121,7 @@ class TitleApiService {
|
||||
RegExp(r'JSESSIONID=([^;,]+)', caseSensitive: false).firstMatch(value);
|
||||
if (match != null) {
|
||||
_cookies = 'JSESSIONID=${match.group(1)!.trim()}';
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] >>> Captured session cookie: $_cookies');
|
||||
ApiLog.log('[$apiName] >>> Captured session cookie: $_cookies');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -135,31 +148,23 @@ class TitleApiService {
|
||||
Future<Map<String, dynamic>> _callApi(
|
||||
String apiName,
|
||||
Map<String, dynamic> packet,
|
||||
int userId,
|
||||
) async {
|
||||
int userId, {
|
||||
bool captureRaw = false,
|
||||
}) async {
|
||||
final hash = _buildHash(apiName);
|
||||
final body = _buildRequestBody(packet);
|
||||
final baseUrl = _normalizeUrl(_config.titleServerUrl);
|
||||
final url = Uri.parse('$baseUrl/$hash');
|
||||
|
||||
// ignore: avoid_print
|
||||
print('══════════════════════════════════════');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] >>> REQUEST >>>');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] URL: $url');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] userId: $userId');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] hash: $hash');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] body (encrypted): ${body.length} bytes');
|
||||
// ignore: avoid_print
|
||||
ApiLog.log('══════════════════════════════════════');
|
||||
ApiLog.log('[$apiName] >>> REQUEST >>>');
|
||||
ApiLog.log('[$apiName] URL: $url');
|
||||
ApiLog.log('[$apiName] userId: $userId');
|
||||
ApiLog.log('[$apiName] hash: $hash');
|
||||
ApiLog.log('[$apiName] body (encrypted): ${body.length} bytes');
|
||||
final packetStr = const JsonEncoder.withIndent(' ').convert(packet);
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] packet (plain):');
|
||||
// ignore: avoid_print
|
||||
print(packetStr);
|
||||
ApiLog.log('[$apiName] packet (plain):');
|
||||
ApiLog.log(packetStr);
|
||||
|
||||
final uaSuffix = userId != 0 ? '$userId' : _config.clientId;
|
||||
final headers = <String, String>{
|
||||
@@ -174,8 +179,7 @@ class TitleApiService {
|
||||
final cookies = _cookies;
|
||||
if (cookies != null) {
|
||||
headers['Cookie'] = cookies;
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] Cookie: $cookies');
|
||||
ApiLog.log('[$apiName] Cookie: $cookies');
|
||||
}
|
||||
|
||||
final response = await http
|
||||
@@ -188,10 +192,8 @@ class TitleApiService {
|
||||
|
||||
_captureCookiesFromResponse(response, apiName);
|
||||
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] HTTP status: ${response.statusCode}');
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] response bytes: ${response.bodyBytes.length}');
|
||||
ApiLog.log('[$apiName] HTTP status: ${response.statusCode}');
|
||||
ApiLog.log('[$apiName] response bytes: ${response.bodyBytes.length}');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw TitleApiException('$apiName returned ${response.statusCode}');
|
||||
@@ -203,12 +205,10 @@ class TitleApiService {
|
||||
|
||||
final json = _processResponseBody(response.bodyBytes);
|
||||
final raw = const JsonEncoder.withIndent(' ').convert(json);
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] <<< RESPONSE <<<');
|
||||
// ignore: avoid_print
|
||||
print(raw);
|
||||
// ignore: avoid_print
|
||||
print('══════════════════════════════════════');
|
||||
if (captureRaw) lastRawResponse = raw;
|
||||
ApiLog.log('[$apiName] <<< RESPONSE <<<');
|
||||
ApiLog.log(raw);
|
||||
ApiLog.log('══════════════════════════════════════');
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -243,99 +243,20 @@ class TitleApiService {
|
||||
required int userId,
|
||||
required String token,
|
||||
}) async {
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] ===== START =====');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] userId=$userId');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] token=$token');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] clientId=${_config.clientId}');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] titleServerUrl=${_config.titleServerUrl}');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] aesKey.len=${_config.aesKeyBytes.length} aesIv.len=${_config.aesIvBytes.length}');
|
||||
|
||||
const apiName = 'GetUserPreviewApi';
|
||||
final hash = _buildHash(apiName);
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] hash=$hash');
|
||||
|
||||
final packet = {
|
||||
'userId': userId,
|
||||
'segaIdAuthKey': '',
|
||||
'token': token,
|
||||
'clientId': _config.clientId,
|
||||
};
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] packet=${jsonEncode(packet)}');
|
||||
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] building request body (compress + encrypt)...');
|
||||
final body = _buildRequestBody(packet);
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] body size=${body.length} bytes');
|
||||
|
||||
final baseUrl = _normalizeUrl(_config.titleServerUrl);
|
||||
final url = Uri.parse('$baseUrl/$hash');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] POST $url');
|
||||
|
||||
try {
|
||||
final previewHeaders = <String, String>{
|
||||
'User-Agent': '$hash#$userId',
|
||||
'Content-Type': 'application/json',
|
||||
'Mai-Encoding': _config.apiVersion,
|
||||
'Accept-Encoding': '',
|
||||
'Charset': 'UTF-8',
|
||||
'Content-Encoding': 'deflate',
|
||||
'number': '0',
|
||||
};
|
||||
final previewCookies = _cookies;
|
||||
if (previewCookies != null) {
|
||||
previewHeaders['Cookie'] = previewCookies;
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] Cookie: $previewCookies');
|
||||
}
|
||||
|
||||
final response = await http
|
||||
.post(
|
||||
url,
|
||||
headers: previewHeaders,
|
||||
body: body,
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] HTTP status=${response.statusCode}');
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] response body size=${response.bodyBytes.length} bytes');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] non-200 response body: ${utf8.decode(response.bodyBytes.take(500).toList())}');
|
||||
throw TitleApiException(
|
||||
'Server returned ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] decrypting + decompressing...');
|
||||
final json = _processResponseBody(response.bodyBytes);
|
||||
final raw = const JsonEncoder.withIndent(' ').convert(json);
|
||||
lastRawResponse = raw;
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] === RESPONSE ===');
|
||||
// ignore: avoid_print
|
||||
print(raw);
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] ===== END =====');
|
||||
return UserPreviewDataBean.fromJson(json);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
print('[getUserPreview] ERROR: $e');
|
||||
rethrow;
|
||||
}
|
||||
// 走通用传输层,保留 `lastRawResponse`(主页调试卡要用 GetUserPreviewApi
|
||||
// 的原文,而不是随后 UserLoginApi / GetUserDataApi 的)。
|
||||
final json = await _callApi(
|
||||
'GetUserPreviewApi',
|
||||
{
|
||||
'userId': userId,
|
||||
'segaIdAuthKey': '',
|
||||
'token': token,
|
||||
'clientId': _config.clientId,
|
||||
},
|
||||
userId,
|
||||
captureRaw: true,
|
||||
);
|
||||
return UserPreviewDataBean.fromJson(json);
|
||||
}
|
||||
|
||||
// ---- Generic data APIs ----
|
||||
|
||||
Reference in new issue
Block a user