fix: correct ticket charge API body, logout type, and login/logout flow

- Fix upsertUserChargeLog: use separate userCharge + userChargelog objects
  matching Lionheart reference, validDate 90 days at 4AM
- Fix userLogout: use LogoutType.Logout = 1 (was incorrectly changed to 0)
- Fix loginDateTime tracking: userLoginFull now returns the exact timestamp
  sent to server, ensuring logout uses matching value
- Fix home page logout: call UserLogoutApi before navigating back
- Add force-logout for stale sessions before ticket flow login
- Add comprehensive debug logging across entire ticket flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 13:10:10 +08:00
parent fee7291ab9
commit 4ad57dc1cf
4 changed files with 467 additions and 153 deletions

View File

@@ -21,11 +21,13 @@ class UserLoginResult {
final String token;
final int loginId;
final String lastLoginDate;
final int loginDateTime;
const UserLoginResult({
required this.token,
required this.loginId,
required this.lastLoginDate,
required this.loginDateTime,
});
}
@@ -109,6 +111,25 @@ class TitleApiService {
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
final packetStr = const JsonEncoder.withIndent(' ').convert(packet);
// ignore: avoid_print
print('[$apiName] packet (plain):');
// ignore: avoid_print
print(packetStr);
final response = await http
.post(
url,
@@ -125,6 +146,11 @@ class TitleApiService {
)
.timeout(const Duration(seconds: 15));
// ignore: avoid_print
print('[$apiName] HTTP status: ${response.statusCode}');
// ignore: avoid_print
print('[$apiName] response bytes: ${response.bodyBytes.length}');
if (response.statusCode != 200) {
throw TitleApiException('$apiName returned ${response.statusCode}');
}
@@ -132,9 +158,11 @@ class TitleApiService {
final json = _processResponseBody(response.bodyBytes);
final raw = const JsonEncoder.withIndent(' ').convert(json);
// ignore: avoid_print
print('[$apiName] === RESPONSE ===');
print('[$apiName] <<< RESPONSE <<<');
// ignore: avoid_print
print(raw);
// ignore: avoid_print
print('══════════════════════════════════════');
return json;
}
@@ -380,9 +408,9 @@ class TitleApiService {
final json = await _callApi(apiName, packet, userId);
final errorId = json['errorId'] as int? ?? -1;
if (errorId != 0) {
throw TitleApiException('UserLoginApi errorId=$errorId');
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UserLoginApi returnCode=$returnCode');
}
final loginId = (json['loginId'] as num?)?.toInt() ?? 0;
@@ -393,6 +421,7 @@ class TitleApiService {
token: newToken,
loginId: loginId,
lastLoginDate: lastLoginDate,
loginDateTime: now,
);
}
@@ -411,13 +440,55 @@ class TitleApiService {
'placeId': _config.placeId,
'clientId': _config.clientId,
'loginDateTime': loginDateTime,
'type': 1,
'type': 1, // LogoutType.Logout
};
final json = await _callApi(apiName, packet, userId);
final errorId = json['errorId'] as int? ?? -1;
if (errorId != 0) {
throw TitleApiException('UserLogoutApi errorId=$errorId');
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UserLogoutApi returnCode=$returnCode');
}
}
// ---- UpsertUserChargeLog (使用功能票) ----
// Reference: Lionheart user.ts charge() method
Future<void> upsertUserChargeLog({
required int userId,
required int ticketId,
required String loginDate,
required int playerRating,
int price = 0,
}) async {
const apiName = 'UpsertUserChargelogApi';
final now = DateTime.now();
// validDate defaults to purchaseDate + 90 days at 4:00 AM
final validDate = DateTime(now.year, now.month, now.day, 4, 0, 0)
.add(const Duration(days: 90));
final purchaseDateStr = _formatDateTime(now);
final packet = {
'userId': userId,
'userCharge': {
'chargeId': ticketId,
'stock': 0,
'purchaseDate': purchaseDateStr,
'validDate': _formatDateTime(validDate),
},
'userChargelog': {
'chargeId': ticketId,
'clientId': _config.clientId,
'regionId': _config.regionId,
'placeId': _config.placeId,
'price': price,
'purchaseDate': purchaseDateStr,
},
};
final json = await _callApi(apiName, packet, userId);
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UpsertUserChargelogApi returnCode=$returnCode');
}
}
@@ -428,6 +499,7 @@ class TitleApiService {
required int loginId,
required Map<String, dynamic> musicData,
required Map<String, dynamic> userData,
int? ticketId,
}) async {
const apiName = 'UploadUserPlaylogListApi';
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
@@ -435,14 +507,14 @@ class TitleApiService {
final packet = {
'userId': userId,
'userPlaylogList': [
_buildPlaylogEntry(userId, loginId, now, musicData, userData),
_buildPlaylogEntry(userId, loginId, now, musicData, userData, ticketId: ticketId),
],
};
final json = await _callApi(apiName, packet, userId);
final errorId = json['errorId'] as int? ?? -1;
if (errorId != 0) {
throw TitleApiException('UploadUserPlaylogListApi errorId=$errorId');
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UploadUserPlaylogListApi returnCode=$returnCode');
}
}
@@ -451,8 +523,9 @@ class TitleApiService {
int loginId,
int timestamp,
Map<String, dynamic> musicData,
Map<String, dynamic> userData,
) {
Map<String, dynamic> userData, {
int? ticketId,
}) {
final user = userData['userData'] as Map<String, dynamic>? ?? {};
final charaSlot = (user['charaSlot'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
@@ -574,12 +647,13 @@ class TitleApiService {
// ---- UpsertUserAll ----
Future<void> upsertUserAll({
Future<Map<String, dynamic>> upsertUserAll({
required int userId,
required int loginId,
required String loginDate,
required Map<String, dynamic> musicData,
required List<Map<String, dynamic>> generalUserInfo,
int? ticketId,
}) async {
const apiName = 'UpsertUserAllApi';
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
@@ -601,7 +675,7 @@ class TitleApiService {
'isFreePlay': false,
'loginDateTime': now,
'userPlaylogList': [
_buildPlaylogEntry(userId, loginId, now, musicData, userData),
_buildPlaylogEntry(userId, loginId, now, musicData, userData, ticketId: ticketId),
],
'upsertUserAll': {
'userData': [
@@ -722,7 +796,7 @@ class TitleApiService {
'version': user['lastRomVersion'] ?? 0,
'playDate': _formatDateTime(DateTime.now()),
'playMode': 0,
'useTicketId': -1,
'useTicketId': ticketId ?? -1,
'playCredit': 1,
'playTrack': 1,
'clientId': _config.clientId,
@@ -764,10 +838,11 @@ class TitleApiService {
};
final json = await _callApi(apiName, packet, userId);
final errorId = json['errorId'] as int? ?? -1;
if (errorId != 0) {
throw TitleApiException('UpsertUserAllApi errorId=$errorId');
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UpsertUserAllApi returnCode=$returnCode');
}
return json;
}
List<Map<String, dynamic>> _buildMissionDataList(