fix: 修改客服,增加sdk使用;增加邀请链接短链,升级flutter到3.35.0,Dart 3.9.0

This commit is contained in:
2026-01-24 23:14:18 -08:00
parent 57a47874be
commit e59cbf8b60
20 changed files with 457 additions and 187 deletions
+13 -1
View File
@@ -41,6 +41,12 @@ class KRAppRunData {
/// 用户账号(使用响应式变量以便 UI 能监听变化)
final Rx<String?> kr_account = Rx<String?>(null);
/// 分享链接(使用响应式变量以便 UI 能监听变化)
final Rx<String?> shareUrl = Rx<String?>(null);
/// 分享链接(使用响应式变量以便 UI 能监听变化)
final Rx<String?> kr_authType = Rx<String?>(null);
/// 用户ID(使用响应式变量以便 UI 能监听变化)
final Rx<int?> kr_userId = Rx<int?>(null);
@@ -633,14 +639,18 @@ class KRAppRunData {
tag: 'AppRunData');
KRLogUtil.kr_i('🆔 [AppRunData] id: ${userInfo.id}',
tag: 'AppRunData');
KRLogUtil.kr_i('🆔 [AppRunData] id: ${userInfo.shareUrl}',
tag: 'AppRunData');
// 保存到全局状态
kr_referCode.value = userInfo.referCode;
kr_refererId.value = userInfo.refererId;
kr_account.value =
authType == 'device' ? '9000${userInfo.id}' : authIdentifier;
authType == 'device' ? '${userInfo.id}' : authIdentifier;
kr_balance.value = userInfo.balance;
kr_commission.value = userInfo.commission;
shareUrl.value = userInfo.shareUrl;
kr_authType.value = authType;
KRLogUtil.kr_i('💾 [AppRunData] 用户信息已保存到全局状态:', tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_referCode: "${kr_referCode.value}"',
@@ -651,6 +661,8 @@ class KRAppRunData {
tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_commission: ${kr_commission.value}',
tag: 'AppRunData');
KRLogUtil.kr_i(' - kr_commission: ${shareUrl.value}',
tag: 'AppRunData');
},
);
} catch (e, stackTrace) {
+3
View File
@@ -31,6 +31,7 @@ class KRUserInfo {
final int referralPercentage;
final bool onlyFirstPurchase;
final int giftAmount;
final String shareUrl;
// 新增 auth_methods 列表
final List<KRAuthMethod> authMethods;
@@ -48,6 +49,7 @@ class KRUserInfo {
this.referralPercentage = 0,
this.onlyFirstPurchase = false,
this.giftAmount = 0,
this.shareUrl = '',
this.authMethods = const [],
});
@@ -65,6 +67,7 @@ class KRUserInfo {
referralPercentage: json['referral_percentage'] ?? 0,
onlyFirstPurchase: json['only_first_purchase'] ?? false,
giftAmount: json['gift_amount'] ?? 0,
shareUrl: json['share_link'] ?? '',
authMethods: (json['auth_methods'] as List<dynamic>?)
?.map((e) => KRAuthMethod.fromJson(e))
.toList() ??
@@ -79,13 +79,8 @@ class HIMenuView extends GetView<HIMenuController> implements HasSwipeConfig {
mainAxisSize: MainAxisSize.min, // 让 Column 包裹内容
children: [
Obx(() {
final account =
KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = (account == null || isDeviceLogin)
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
final authType = KRAppRunData.getInstance().kr_authType.value;
final accountText = authType == 'email' ? '${KRAppRunData.getInstance().kr_account.value.toString()}' : '待绑定';
return UserInfoCard(
controller: controller,
userId: accountText,
@@ -1,21 +1,69 @@
import 'package:get/get.dart';
import 'package:chatwoot_flutter_sdk/chatwoot_sdk.dart';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
/// 客服2Chatwoot)控制器
class KRChatwootController extends GetxController {
/// Chatwoot 客服链接
final String kr_chatwootUrl = 'https://app.chatwoot.com/widget?website_token=YXQmh16ymNYW1SVybhnoQQ9w';
import 'package:kaer_with_panels/app/utils/kr_secure_storage.dart';
/// 加载状态
final RxBool kr_isLoading = true.obs;
class KRChatwootController {
// 配置参数
final String baseUrl = 'https://app.chatwoot.com';
final String inboxIdentifier = 'YXQmh16ymNYW1SVybhnoQQ9w';
@override
void onInit() {
super.onInit();
KRChatwootController() {
KRSecureStorage().kr_readData(key: 'USER_INFO').then((value) {
print('KRSecureStorage_keyUserInfo: $value');
});
}
/// 设置加载状态
/// [loading] 是否正在加载
void kr_setLoading(bool loading) {
kr_isLoading.value = loading;
/// 获取当前用户信息
/// 建议从你的全局 UserState 或本地存储中动态获取
ChatwootUser get chatUser {
final appRunData = KRAppRunData.getInstance();
final userId = appRunData.kr_userId.value?.toString();
final deviceId = appRunData.deviceId;
final account = appRunData.kr_account.value;
// 优先使用 userId 作为唯一标识,如果没有则使用 deviceId
final identifier = userId ?? deviceId ?? 'unknown_user';
// 名字使用账号,如果没有则显示 Guest
final name = account ?? 'Guest';
// 邮箱如果是邮箱格式则使用,否则构造一个唯一的虚拟邮箱以满足格式要求
String email = '';
if (account != null && account.contains('@')) {
email = account;
} else {
email = '$identifier@hifastvpn.com';
}
return ChatwootUser(
identifier: identifier, // 必填:用于关联历史聊天记录
name: name,
email: email,
);
}
}
/// 处理文件/附件选择逻辑
Future<List<String>> onFilePicker() async {
try {
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.any,
);
if (result != null && result.paths.isNotEmpty) {
// 过滤掉 null 路径并转换为 String 列表
return result.paths.whereType<String>().toList();
}
} catch (e) {
debugPrint("KRChatwoot: 文件选择失败 - $e");
}
return [];
}
// 可以在这里添加更多业务方法,例如:
// void trackEvent(String eventName) { ... }
}
@@ -1,52 +1,71 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import 'package:kaer_with_panels/app/widgets/kr_simple_loading.dart';
import 'package:chatwoot_flutter_sdk/chatwoot_sdk.dart';
import '../controllers/kr_chatwoot_controller.dart';
import 'package:get/get.dart';
/// 客服2Chatwoot)视图页面
class KRChatwootView extends GetView<KRChatwootController> {
class KRChatwootView extends StatefulWidget {
const KRChatwootView({super.key});
@override
State<KRChatwootView> createState() => _KRChatwootViewState();
}
class _KRChatwootViewState extends State<KRChatwootView> {
// 实例化控制器
final KRChatwootController _controller = KRChatwootController();
// 控制加载遮罩状态
bool _isPageLoading = true;
@override
Widget build(BuildContext context) {
return HIBaseScaffold(
showBack: true,
topContentAreaHeight: 0,
child: Stack(
children: [
InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(controller.kr_chatwootUrl),
return Scaffold(
// 设置背景色为白色,确保顶部 20px 区域不漏出底色
backgroundColor: Colors.white,
body: Padding(
// 在这里设置顶部 20px 的间距
padding: const EdgeInsets.only(top: 20.0),
child: Stack(
children: [
ChatwootWidget(
baseUrl: _controller.baseUrl,
websiteToken: _controller.inboxIdentifier,
user: _controller.chatUser,
locale: "zh_CN",
onAttachFile: _controller.onFilePicker,
onLoadStarted: () {
debugPrint("KRChatwoot: 开始加载 SDK");
},
closeWidget: () {
print('返回');
Get.back();
},
onLoadCompleted: () {
debugPrint("KRChatwoot: SDK 加载完毕");
if (mounted) {
setState(() => _isPageLoading = false);
}
},
),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
domStorageEnabled: true,
allowsInlineMediaPlayback: true,
),
onLoadStart: (controller, url) {
this.controller.kr_setLoading(true);
},
onLoadStop: (controller, url) {
this.controller.kr_setLoading(false);
},
onReceivedError: (controller, request, error) {
this.controller.kr_setLoading(false);
},
),
Obx(() {
if (controller.kr_isLoading.value) {
return const Center(
child: KRSimpleLoading(
size: 40,
// 加载中的占位图
if (_isPageLoading)
Container(
color: Colors.white,
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(strokeWidth: 2),
const SizedBox(height: 16),
Text("正在连接客服...", style: TextStyle(color: Colors.grey)),
],
),
),
);
}
return const SizedBox.shrink();
}),
],
),
],
),
),
);
}
}
}
@@ -121,12 +121,8 @@ class KRDeleteAccountView extends GetView<KRDeleteAccountController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = (account ==null || isDeviceLogin)
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
final authType = KRAppRunData.getInstance().kr_authType.value;
final accountText = authType == 'email' ? '${KRAppRunData.getInstance().kr_account.value.toString()}' : '待绑定';
return Text(
accountText,
style: TextStyle(
@@ -10,6 +10,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/widgets/hi_base_scaffold.dart';
import 'package:kaer_with_panels/app/widgets/hi_help_entrance.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
class KRInviteView extends GetView<KRInviteController> {
const KRInviteView({super.key});
@@ -123,14 +124,14 @@ class KRInviteView extends GetView<KRInviteController> {
color: Colors.white,
),
onPressed: () {
if (controller.kr_referCode.value.isNotEmpty) {
final code = controller.kr_referCode.value;
final text = '#您的好友邀请您使用Hi快网络加速器\n'
'安装完毕后,在软件内<邀请好友>页面粘贴以下邀请码\n'
'$code\n'
'您和您的好友将会分别获得3天免费时长\n\n'
'点击此处进入下载页面\n'
'或在浏览器输入hifastvpn.com下载#';
'首次付款后您和您的好友将会分别获得3天免费时长\n'
'请在浏览器输入${KRAppRunData.getInstance().shareUrl.value}下载#';
if (GetPlatform.isIOS) {
Share.share(text, subject: '直接分享Hi快VPN邀请链接');
} else {
@@ -86,12 +86,8 @@ class KRLoginView extends GetView<KRLoginController> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
final account = KRAppRunData.getInstance().kr_account.value;
final isDeviceLogin =
account != null && account.startsWith('9000');
final accountText = (account == null || isDeviceLogin)
? '待绑定'
: '${KRAppRunData.getInstance().kr_account.value.toString()}';
final authType = KRAppRunData.getInstance().kr_authType.value;
final accountText = authType == 'email' ? '${KRAppRunData.getInstance().kr_account.value.toString()}' : '待绑定';
return Text(
accountText,
+1 -1
View File
@@ -259,7 +259,7 @@ class HttpUtil {
/// 自定义简洁 HTTP 拦截器(无边框符号)
class _KRSimpleHttpInterceptor extends Interceptor {
/// 常量:手动控制是否打印拦截器日志与解密结果
static const bool KR_HTTP_PRINT = false;
static const bool KR_HTTP_PRINT = true;
final Dio _dio;
_KRSimpleHttpInterceptor(this._dio);
static String? _lastPath;
@@ -1,29 +1,42 @@
import 'package:flutter_keychain/flutter_keychain.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class IAPPendingOrderService {
static const _keyOrderNo = 'hi_iap_pending_order_no';
static const _keyJwsData = 'hi_iap_pending_jws_data'; // 新增 Key
// 创建统一存储实例
static final _storage = FlutterSecureStorage(
aOptions: const AndroidOptions(
encryptedSharedPreferences: true, // Android 加密存储
),
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.first_unlock, // iOS Keychain
),
);
/// 存储待处理订单号
static Future<void> setPendingOrderNo(String orderNo) async {
await FlutterKeychain.put(key: _keyOrderNo, value: orderNo);
await _storage.write(key: _keyOrderNo, value: orderNo);
}
/// 获取待处理订单号
static Future<String?> getPendingOrderNo() async {
return await FlutterKeychain.get(key: _keyOrderNo);
return await _storage.read(key: _keyOrderNo);
}
/// 存储 JWS 凭证数据
static Future<void> setPendingJwsData(String jwsData) async {
await FlutterKeychain.put(key: _keyJwsData, value: jwsData);
await _storage.write(key: _keyJwsData, value: jwsData);
}
/// 获取存储的 JWS 凭证数据
static Future<String?> getPendingJwsData() async {
return await FlutterKeychain.get(key: _keyJwsData);
return await _storage.read(key: _keyJwsData);
}
/// 清除待处理订单和 JWS 数据
static Future<void> clearPendingOrderNo() async {
await FlutterKeychain.remove(key: _keyOrderNo);
await FlutterKeychain.remove(key: _keyJwsData);
await _storage.delete(key: _keyOrderNo);
await _storage.delete(key: _keyJwsData);
}
}
+17 -22
View File
@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:flutter_keychain/flutter_keychain.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../utils/kr_secure_storage.dart';
import '../utils/kr_log_util.dart';
import 'package:crypto/crypto.dart';
@@ -140,54 +140,49 @@ class KRDeviceInfoService {
/// iOS设备ID - 优先使用Keychain存储的UUID(卸载重装后不变)
/// iOS的identifierForVendor在所有同供应商应用卸载后会重置
/// 使用flutter_keychain存储到iOS Keychain,保证即使卸载重装,设备ID也不会变化
/// 使用flutter_secure_storage存储到iOS Keychain,保证即使卸载重装,设备ID也不会变化
Future<String> _getIOSDeviceId() async {
try {
const keychainKey = 'kr_ios_device_persistent_id';
// 1. 优先从iOS Keychain读取已存储的设备ID(卸载后依然存在)
String? storedId = await FlutterKeychain.get(key: keychainKey);
final storage = FlutterSecureStorage();
// 1. 优先从 Keychain 读取已存储的设备ID
String? storedId = await storage.read(key: keychainKey);
if (storedId != null && storedId.isNotEmpty) {
if (kDebugMode) {
print('📱 iOS: 从Keychain读取已存储的设备ID');
print('📱 iOS: 从 Keychain 读取已存储的设备ID');
}
KRLogUtil.kr_i('📱 iOS: 使用Keychain中的持久化设备ID', tag: 'KRDeviceInfoService');
KRLogUtil.kr_i('📱 iOS: 使用 Keychain 中的持久化设备ID', tag: 'KRDeviceInfoService');
return storedId;
}
// 2. 如果Keychain中没有,生成新的设备ID
// 2. 如果 Keychain 中没有,生成新的设备ID
final iosInfo = await _deviceInfo.iosInfo;
// 使用硬件级别的信息生成唯一标识(作为备用方案)
String udid = await FlutterUdid.consistentUdid;
// 构建多因子字符串(不使用会变化的identifierForVendor作为主要因子)
final factors = [
udid,
iosInfo.utsname.machine, // 硬件机器类型(如"iPhone14,3"
iosInfo.model, // 型号
iosInfo.systemName, // 系统名
// 移除: iosInfo.identifierForVendor - 因为它会在卸载后变化
// 移除: iosInfo.name - 用户可以修改设备名
// 移除: iosInfo.systemVersion - 系统升级会变化
iosInfo.utsname.machine,
iosInfo.model,
iosInfo.systemName,
];
final combined = factors
.where((f) => f.isNotEmpty)
.join('|');
final combined = factors.where((f) => f.isNotEmpty).join('|');
final bytes = utf8.encode(combined);
final hash = sha256.convert(bytes);
final newDeviceId = hash.toString();
// 3. 保存到iOS Keychain(即使应用卸载也会保留)
await FlutterKeychain.put(key: keychainKey, value: newDeviceId);
// 3. 保存到 Keychain
await storage.write(key: keychainKey, value: newDeviceId);
if (kDebugMode) {
print('📱 iOS: 生成新设备ID并保存到Keychain - 因子数: ${factors.where((f) => f.isNotEmpty).length}');
print('📱 iOS: 生成新设备ID并保存到 Keychain');
}
KRLogUtil.kr_i('📱 iOS: 生成并持久化新设备ID到Keychain', tag: 'KRDeviceInfoService');
KRLogUtil.kr_i('📱 iOS: 新设备ID已保存到 Keychain', tag: 'KRDeviceInfoService');
return newDeviceId;
} catch (e) {