vip 接口 游戏及 重新安装等问题
This commit is contained in:
parent
7d12417116
commit
9f4df82c42
@ -38,11 +38,15 @@
|
||||
</intent>
|
||||
<package android:name="com.snapchat.android" />
|
||||
</queries>
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:icon="${appIcon}"
|
||||
android:label="${appName}"
|
||||
android:usesCleartextTraffic="true">
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="${appIcon}"
|
||||
android:label="${appName}"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:allowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
|
||||
6
android/app/src/main/res/xml/backup_rules.xml
Normal file
6
android/app/src/main/res/xml/backup_rules.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
</full-backup-content>
|
||||
13
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
13
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include
|
||||
domain="sharedpref"
|
||||
path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@ -1,13 +1,91 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import Flutter
|
||||
import Security
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
registerDurableAuthStorageChannel()
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
private func registerDurableAuthStorageChannel() {
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
return
|
||||
}
|
||||
let channel = FlutterMethodChannel(
|
||||
name: "com.org.yumiparty/durable_auth_storage",
|
||||
binaryMessenger: controller.binaryMessenger
|
||||
)
|
||||
channel.setMethodCallHandler { [weak self] call, result in
|
||||
guard
|
||||
let self,
|
||||
let args = call.arguments as? [String: Any],
|
||||
let key = args["key"] as? String
|
||||
else {
|
||||
result(FlutterError(code: "bad_args", message: "Missing key", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
switch call.method {
|
||||
case "write":
|
||||
guard let value = args["value"] as? String else {
|
||||
result(FlutterError(code: "bad_args", message: "Missing value", details: nil))
|
||||
return
|
||||
}
|
||||
self.writeKeychainValue(value, key: key)
|
||||
result(nil)
|
||||
case "read":
|
||||
result(self.readKeychainValue(key: key))
|
||||
case "delete":
|
||||
self.deleteKeychainValue(key: key)
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func keychainQuery(key: String) -> [String: Any] {
|
||||
[
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: "com.org.yumiparty.durable_auth_storage",
|
||||
kSecAttrAccount as String: key
|
||||
]
|
||||
}
|
||||
|
||||
private func writeKeychainValue(_ value: String, key: String) {
|
||||
deleteKeychainValue(key: key)
|
||||
guard let data = value.data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
var query = keychainQuery(key: key)
|
||||
query[kSecValueData as String] = data
|
||||
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
SecItemAdd(query as CFDictionary, nil)
|
||||
}
|
||||
|
||||
private func readKeychainValue(key: String) -> String? {
|
||||
var query = keychainQuery(key: key)
|
||||
query[kSecReturnData as String] = true
|
||||
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard
|
||||
status == errSecSuccess,
|
||||
let data = result as? Data
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func deleteKeychainValue(key: String) {
|
||||
SecItemDelete(keychainQuery(key: key) as CFDictionary)
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import 'app/routes/sc_routes.dart';
|
||||
import 'app/routes/sc_lk_application.dart';
|
||||
import 'shared/tools/sc_deep_link_handler.dart';
|
||||
import 'shared/tools/sc_deviceId_utils.dart';
|
||||
import 'shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'modules/splash/splash_page.dart';
|
||||
import 'services/general/sc_app_general_manager.dart';
|
||||
import 'services/payment/apple_payment_manager.dart';
|
||||
@ -94,6 +95,7 @@ Future<void> _prepareAppShell() async {
|
||||
SCKeybordUtil.init();
|
||||
// 使用重试机制初始化存储
|
||||
await _initStore();
|
||||
await AccountStorage().restoreDurableSessionIfNeeded();
|
||||
await SCDeviceIdUtils.initDeviceinfo();
|
||||
_configureImageCache();
|
||||
}
|
||||
|
||||
@ -346,24 +346,23 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double _calculateBodyHeight(BuildContext context, double headerHeight) {
|
||||
double _calculateBodyHeight(BuildContext context) {
|
||||
final screenMode = _resolveScreenMode();
|
||||
final maxDialogHeight = resolveRoomGameAvailableHeight(
|
||||
context,
|
||||
reservedTop: 24.w,
|
||||
reservedTop: 12.w,
|
||||
);
|
||||
final maxBodyHeight = maxDialogHeight - headerHeight;
|
||||
if (maxBodyHeight <= 0) {
|
||||
if (maxDialogHeight <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (screenMode == 'full') {
|
||||
return maxBodyHeight;
|
||||
return maxDialogHeight;
|
||||
}
|
||||
|
||||
return clampRoomGameHeight(
|
||||
_calculatePreferredBodyHeight(context),
|
||||
maxBodyHeight,
|
||||
maxDialogHeight,
|
||||
);
|
||||
}
|
||||
|
||||
@ -544,8 +543,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final headerHeight = 44.w;
|
||||
final bodyHeight = _calculateBodyHeight(context, headerHeight);
|
||||
final bodyHeight = _calculateBodyHeight(context);
|
||||
final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context);
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
@ -566,7 +564,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
),
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: bodyHeight + headerHeight + bottomFrameHeight,
|
||||
height: bodyHeight + bottomFrameHeight,
|
||||
color: const Color(0xFF081915),
|
||||
child: Stack(
|
||||
children: [
|
||||
@ -574,13 +572,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: headerHeight,
|
||||
child: _buildHeader(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: headerHeight,
|
||||
bottom: bottomFrameHeight,
|
||||
child: WebViewWidget(
|
||||
controller: _controller,
|
||||
@ -614,36 +605,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
color: const Color(0xFF102D28),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.game.name.isEmpty ? 'Leader Game' : widget.game.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
unawaited(_closeAndExit());
|
||||
},
|
||||
icon: Icon(Icons.close, color: Colors.white, size: 20.w),
|
||||
splashRadius: 18.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -6,7 +7,11 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/tools/sc_version_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_google_auth_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_auth_type.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||
@ -448,6 +453,15 @@ class _SplashPageState extends State<SplashPage> {
|
||||
var token = AccountStorage().getToken();
|
||||
if (user != null && token.isNotEmpty) {
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
return;
|
||||
}
|
||||
final restoredUser = await _tryRestoreLoginSession();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (restoredUser != null &&
|
||||
(restoredUser.token ?? "").trim().isNotEmpty) {
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
} else {
|
||||
SCNavigatorUtils.pushLoginIfNeeded(context, replace: true);
|
||||
}
|
||||
@ -459,6 +473,28 @@ class _SplashPageState extends State<SplashPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SocialChatLoginRes?> _tryRestoreLoginSession() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final googleUser = await SCGoogleAuthService.signInSilently();
|
||||
final uid = googleUser?.uid ?? "";
|
||||
if (uid.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final user = await SCAccountRepository().loginForChannel(
|
||||
SCAuthType.GOOGLE.name,
|
||||
uid,
|
||||
);
|
||||
AccountStorage().setCurrentUser(user);
|
||||
return user;
|
||||
} catch (e) {
|
||||
debugPrint('restore login session failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消倒计时的计时器。
|
||||
void _cancelTimer() {
|
||||
// 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。
|
||||
|
||||
@ -58,6 +58,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
final status = results[1] as SCVipStatusRes;
|
||||
final resolvedStatus = status.levelInt > 0 ? status : home.state;
|
||||
final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
|
||||
_logLoadedVipData(home, resolvedStatus, selectedLevel);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@ -122,6 +123,11 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
if (_home?.configured == false ||
|
||||
config?.enabled == false ||
|
||||
config?.canPurchase != true) {
|
||||
debugPrint(
|
||||
'[VIP][Page] skip preview level=$level '
|
||||
'configured=${_home?.configured} enabled=${config?.enabled} '
|
||||
'canPurchase=${config?.canPurchase}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -163,6 +169,10 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
|
||||
if (!mounted) return;
|
||||
if (purchase.success == true) {
|
||||
debugPrint(
|
||||
'[VIP][Page] purchase success targetLevel=$_selectedLevel '
|
||||
'orderId=${purchase.order?.id} paidGold=${purchase.order?.paidGold}',
|
||||
);
|
||||
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
|
||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
profileManager.balance();
|
||||
@ -183,6 +193,31 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
void _logLoadedVipData(
|
||||
SCVipHomeRes home,
|
||||
SCVipStatusRes? status,
|
||||
int selectedLevel,
|
||||
) {
|
||||
debugPrint(
|
||||
'[VIP][Page] home loaded configured=${home.configured} '
|
||||
'levels=${home.levels.length} selectedLevel=$selectedLevel '
|
||||
'statusActive=${status?.active} statusLevel=${status?.level} '
|
||||
'statusExpireAt=${status?.expireAt}',
|
||||
);
|
||||
if (home.configured != true) {
|
||||
debugPrint(
|
||||
'[VIP][Page][Warn] vip home configured=${home.configured}; '
|
||||
'check backend VIP config for sysOrigin=${home.sysOrigin}',
|
||||
);
|
||||
}
|
||||
if (home.levels.isEmpty) {
|
||||
debugPrint(
|
||||
'[VIP][Page][Warn] vip home levels is empty; '
|
||||
'frontend has no level data to render.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class DurableAuthStorage {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'com.org.yumiparty/durable_auth_storage',
|
||||
);
|
||||
static const String _sessionKey = 'auth_session_v1';
|
||||
|
||||
static Future<void> saveSession({
|
||||
required String token,
|
||||
required String currentUserJson,
|
||||
}) async {
|
||||
if (!Platform.isIOS) {
|
||||
return;
|
||||
}
|
||||
if (token.trim().isEmpty || currentUserJson.trim().isEmpty) {
|
||||
await clearSession();
|
||||
return;
|
||||
}
|
||||
await _invokeSafely('write', <String, Object>{
|
||||
'key': _sessionKey,
|
||||
'value': jsonEncode(<String, String>{
|
||||
'token': token,
|
||||
'currentUser': currentUserJson,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
static Future<DurableAuthSession?> readSession() async {
|
||||
if (!Platform.isIOS) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final raw = await _channel.invokeMethod<String>('read', <String, Object>{
|
||||
'key': _sessionKey,
|
||||
});
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final json = jsonDecode(raw);
|
||||
if (json is! Map) {
|
||||
return null;
|
||||
}
|
||||
final token = (json['token'] as String? ?? '').trim();
|
||||
final currentUserJson = json['currentUser'] as String? ?? '';
|
||||
if (token.isEmpty || currentUserJson.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DurableAuthSession(token: token, currentUserJson: currentUserJson);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> clearSession() async {
|
||||
if (!Platform.isIOS) {
|
||||
return;
|
||||
}
|
||||
await _invokeSafely('delete', <String, Object>{'key': _sessionKey});
|
||||
}
|
||||
|
||||
static Future<void> _invokeSafely(
|
||||
String method,
|
||||
Map<String, Object> arguments,
|
||||
) async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>(method, arguments);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
class DurableAuthSession {
|
||||
const DurableAuthSession({
|
||||
required this.token,
|
||||
required this.currentUserJson,
|
||||
});
|
||||
|
||||
final String token;
|
||||
final String currentUserJson;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@ -5,6 +6,7 @@ import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/durable_auth_storage.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
@ -48,6 +50,12 @@ class AccountStorage {
|
||||
String userJson = jsonEncode(_currentUser?.toJson());
|
||||
if (userJson.isNotEmpty) {
|
||||
DataPersistence.setCurrentUser(userJson);
|
||||
unawaited(
|
||||
DurableAuthStorage.saveSession(
|
||||
token: _currentUser!.token ?? "",
|
||||
currentUserJson: userJson,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +127,30 @@ class AccountStorage {
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<void> restoreDurableSessionIfNeeded() async {
|
||||
final localToken = DataPersistence.getToken().trim();
|
||||
final localUserJson = DataPersistence.getCurrentUser().trim();
|
||||
if (localToken.isNotEmpty && localUserJson.isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final session = await DurableAuthStorage.readSession();
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
token = session.token;
|
||||
await DataPersistence.setToken(session.token);
|
||||
await DataPersistence.setCurrentUser(session.currentUserJson);
|
||||
try {
|
||||
_currentUser = SocialChatLoginRes.fromJson(
|
||||
jsonDecode(session.currentUserJson),
|
||||
);
|
||||
} catch (_) {
|
||||
_currentUser = null;
|
||||
}
|
||||
}
|
||||
|
||||
void setToken(String tk) {
|
||||
token = tk;
|
||||
DataPersistence.setToken(token);
|
||||
@ -129,6 +161,7 @@ class AccountStorage {
|
||||
_currentUser = null;
|
||||
DataPersistence.setToken("");
|
||||
DataPersistence.setCurrentUser("");
|
||||
unawaited(DurableAuthStorage.clearSession());
|
||||
}
|
||||
|
||||
///退出登录
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/shared/business_logic/repositories/vip_repository.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart';
|
||||
|
||||
const String _vipApiHost = String.fromEnvironment(
|
||||
'VIP_API_HOST',
|
||||
defaultValue: '',
|
||||
);
|
||||
|
||||
class SCVipRepositoryImp implements SocialChatVipRepository {
|
||||
static SCVipRepositoryImp? _instance;
|
||||
|
||||
@ -13,20 +23,38 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
|
||||
|
||||
@override
|
||||
Future<SCVipStatusRes> vipStatus() async {
|
||||
final result = await http.get<SCVipStatusRes>(
|
||||
'/app/vip/status',
|
||||
fromJson: (json) => SCVipStatusRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
final path = _vipPath('/status');
|
||||
_logRequest('GET', path);
|
||||
try {
|
||||
final result = await http.get<SCVipStatusRes>(
|
||||
path,
|
||||
extra: _vipExtra(),
|
||||
fromJson: (json) => SCVipStatusRes.fromJson(json),
|
||||
);
|
||||
_logResponse('GET', path, _statusSummary(result));
|
||||
return result;
|
||||
} catch (error) {
|
||||
_logError('GET', path, error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCVipHomeRes> vipHome() async {
|
||||
final result = await http.get<SCVipHomeRes>(
|
||||
'/app/vip/home',
|
||||
fromJson: (json) => SCVipHomeRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
final path = _vipPath('/home');
|
||||
_logRequest('GET', path);
|
||||
try {
|
||||
final result = await http.get<SCVipHomeRes>(
|
||||
path,
|
||||
extra: _vipExtra(),
|
||||
fromJson: (json) => SCVipHomeRes.fromJson(json),
|
||||
);
|
||||
_logResponse('GET', path, _homeSummary(result));
|
||||
return result;
|
||||
} catch (error) {
|
||||
_logError('GET', path, error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -46,12 +74,21 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
|
||||
params['targetLevelCode'] = targetLevelCode;
|
||||
}
|
||||
|
||||
final result = await http.post<SCVipPreviewRes>(
|
||||
'/app/vip/preview',
|
||||
data: params,
|
||||
fromJson: (json) => SCVipPreviewRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
final path = _vipPath('/preview');
|
||||
_logRequest('POST', path, body: params);
|
||||
try {
|
||||
final result = await http.post<SCVipPreviewRes>(
|
||||
path,
|
||||
data: params,
|
||||
extra: _vipExtra(),
|
||||
fromJson: (json) => SCVipPreviewRes.fromJson(json),
|
||||
);
|
||||
_logResponse('POST', path, _previewSummary(result));
|
||||
return result;
|
||||
} catch (error) {
|
||||
_logError('POST', path, error, body: params);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -59,21 +96,195 @@ class SCVipRepositoryImp implements SocialChatVipRepository {
|
||||
required int targetLevel,
|
||||
required String bizIdempotentKey,
|
||||
}) async {
|
||||
final result = await http.post<SCVipPurchaseRes>(
|
||||
'/app/vip/purchase',
|
||||
data: {'targetLevel': targetLevel, 'bizIdempotentKey': bizIdempotentKey},
|
||||
fromJson: (json) => SCVipPurchaseRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
final path = _vipPath('/purchase');
|
||||
final body = {
|
||||
'targetLevel': targetLevel,
|
||||
'bizIdempotentKey': bizIdempotentKey,
|
||||
};
|
||||
_logRequest('POST', path, body: body);
|
||||
try {
|
||||
final result = await http.post<SCVipPurchaseRes>(
|
||||
path,
|
||||
data: body,
|
||||
extra: _vipExtra(),
|
||||
fromJson: (json) => SCVipPurchaseRes.fromJson(json),
|
||||
);
|
||||
_logResponse('POST', path, _purchaseSummary(result));
|
||||
return result;
|
||||
} catch (error) {
|
||||
_logError('POST', path, error, body: body);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SCVipOrdersRes> vipOrders({int cursor = 1, int limit = 20}) async {
|
||||
final result = await http.get<SCVipOrdersRes>(
|
||||
'/app/vip/orders',
|
||||
queryParams: {'cursor': cursor, 'limit': limit},
|
||||
fromJson: (json) => SCVipOrdersRes.fromJson(json),
|
||||
final path = _vipPath('/orders');
|
||||
final query = {'cursor': cursor, 'limit': limit};
|
||||
_logRequest('GET', path, query: query);
|
||||
try {
|
||||
final result = await http.get<SCVipOrdersRes>(
|
||||
path,
|
||||
queryParams: query,
|
||||
extra: _vipExtra(),
|
||||
fromJson: (json) => SCVipOrdersRes.fromJson(json),
|
||||
);
|
||||
_logResponse('GET', path, _ordersSummary(result));
|
||||
return result;
|
||||
} catch (error) {
|
||||
_logError('GET', path, error, query: query);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static bool get _hasExplicitVipApiHost => _vipApiHost.trim().isNotEmpty;
|
||||
|
||||
static String get _activeBaseUrl =>
|
||||
_hasExplicitVipApiHost ? _vipApiHost.trim() : SCGlobalConfig.apiHost;
|
||||
|
||||
static String _vipPath(String suffix) {
|
||||
final baseUrl = _activeBaseUrl.trim();
|
||||
final parsed = Uri.tryParse(baseUrl);
|
||||
final pathSegments =
|
||||
parsed?.pathSegments.where((segment) => segment.isNotEmpty).toSet() ??
|
||||
<String>{};
|
||||
final baseAlreadyContainsGo = pathSegments.contains('go');
|
||||
|
||||
if (_hasExplicitVipApiHost || baseAlreadyContainsGo) {
|
||||
return '/app/vip$suffix';
|
||||
}
|
||||
return '/go/app/vip$suffix';
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _vipExtra() {
|
||||
if (!_hasExplicitVipApiHost) {
|
||||
return null;
|
||||
}
|
||||
return {BaseNetworkClient.baseUrlOverrideKey: _vipApiHost.trim()};
|
||||
}
|
||||
|
||||
static void _logRequest(
|
||||
String method,
|
||||
String path, {
|
||||
Map<String, dynamic>? query,
|
||||
Map<String, dynamic>? body,
|
||||
}) {
|
||||
debugPrint(
|
||||
'[VIP][Request] $method $path '
|
||||
'baseUrl=$_activeBaseUrl query=${_json(query ?? const {})} '
|
||||
'body=${_json(body ?? const {})}',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void _logResponse(
|
||||
String method,
|
||||
String path,
|
||||
Map<String, dynamic> summary,
|
||||
) {
|
||||
debugPrint('[VIP][Response] $method $path summary=${_json(summary)}');
|
||||
}
|
||||
|
||||
static void _logError(
|
||||
String method,
|
||||
String path,
|
||||
Object error, {
|
||||
Map<String, dynamic>? query,
|
||||
Map<String, dynamic>? body,
|
||||
}) {
|
||||
debugPrint(
|
||||
'[VIP][Error] $method $path query=${_json(query ?? const {})} '
|
||||
'body=${_json(body ?? const {})} error=$error',
|
||||
);
|
||||
if (error is DioException) {
|
||||
debugPrint(
|
||||
'[VIP][DioError] $method $path '
|
||||
'uri=${error.requestOptions.uri} '
|
||||
'statusCode=${error.response?.statusCode} '
|
||||
'message=${error.message} '
|
||||
'response=${_json(error.response?.data)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _statusSummary(SCVipStatusRes result) {
|
||||
return {
|
||||
'active': result.active,
|
||||
'status': result.status,
|
||||
'level': result.level,
|
||||
'levelCode': result.levelCode,
|
||||
'expireAt': result.expireAt,
|
||||
'hasBadge': result.badge?.url?.isNotEmpty == true,
|
||||
'hasAvatarFrame': result.avatarFrame?.url?.isNotEmpty == true,
|
||||
'hasEntryEffect': result.entryEffect?.url?.isNotEmpty == true,
|
||||
'hasChatBubble': result.chatBubble?.url?.isNotEmpty == true,
|
||||
'hasFloatPicture': result.floatPicture?.url?.isNotEmpty == true,
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _homeSummary(SCVipHomeRes result) {
|
||||
return {
|
||||
'configured': result.configured,
|
||||
'state': result.state == null ? null : _statusSummary(result.state!),
|
||||
'levelsCount': result.levels.length,
|
||||
'levels':
|
||||
result.levels
|
||||
.map(
|
||||
(level) => {
|
||||
'level': level.level,
|
||||
'levelCode': level.levelCode,
|
||||
'enabled': level.enabled,
|
||||
'durationDays': level.durationDays,
|
||||
'priceGold': level.priceGold,
|
||||
'canPurchase': level.canPurchase,
|
||||
'payableGold': level.payableGold,
|
||||
'hasBadge': level.badge?.url?.isNotEmpty == true,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _previewSummary(SCVipPreviewRes result) {
|
||||
return {
|
||||
'orderType': result.orderType,
|
||||
'currentLevel': result.currentLevel,
|
||||
'targetLevel': result.targetLevel,
|
||||
'durationDays': result.durationDays,
|
||||
'originalPriceGold': result.originalPriceGold,
|
||||
'payableGold': result.payableGold,
|
||||
'expireAt': result.expireAt,
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _purchaseSummary(SCVipPurchaseRes result) {
|
||||
final order = result.order;
|
||||
return {
|
||||
'success': result.success,
|
||||
'order':
|
||||
order == null
|
||||
? null
|
||||
: {
|
||||
'id': order.id,
|
||||
'orderType': order.orderType,
|
||||
'fromLevel': order.fromLevel,
|
||||
'toLevel': order.toLevel,
|
||||
'paidGold': order.paidGold,
|
||||
'status': order.status,
|
||||
'expireAt': order.expireAt,
|
||||
},
|
||||
'state': result.state == null ? null : _statusSummary(result.state!),
|
||||
};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _ordersSummary(SCVipOrdersRes result) {
|
||||
return {'recordsCount': result.records.length, 'total': result.total};
|
||||
}
|
||||
|
||||
static String _json(dynamic value) {
|
||||
try {
|
||||
return jsonEncode(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user