feat: 源码提交
Build Windows / build (push) Has been cancelled

This commit is contained in:
2025-10-19 23:30:54 -07:00
parent 54e7df78db
commit 75d4c48e41
6076 changed files with 64655 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
/// 接口名称
abstract class Api {
/// 游客登录查看是否已经注册
static const String kr_isRegister = "/v1/app/auth/check";
/// 注册
static const String kr_register = "/v1/auth/register";
/// 验证验证码
static const String kr_checkVerificationCode = "/v1/auth/check-code";
/// 发送验证码(统一接口,支持邮箱和手机)
static const String kr_sendCode = "/v1/auth/send-code";
/// 登录接口
static const String kr_login = "/v1/auth/login";
/// 设备登录(游客登录)
/// 参考 OmnOem 项目 ppanel.json 配置
static const String kr_deviceLogin = "/v1/auth/login/device";
/// 删除账号
static const String kr_deleteAccount = "/v1/app/user/account";
/// 忘记密码-设置新密码
static const String kr_setNewPsdByForgetPsd = "/v1/app/auth/reset_password";
/// 节点信息(包含试用/付费标志)
static const String kr_nodeList = "/v1/public/subscribe/node/list";
/// 获取用户订阅流量日志
static const String kr_nodeGroupList = "/v1/app/node/rule_group_list";
/// 预下单
static const String kr_preOrder = "/v1/app/order/pre";
/// 获取下单zf方式
static const String kr_getPaymentMethods = "/v1/app/payment/methods";
/// 进行下单
static const String kr_purchase = "/v1/app/order/purchase";
/// 获取支付地址,跳转到付款地址
static const String kr_checkout = "/v1/app/order/checkout";
/// 获取可购买套餐
static const String kr_getPackageList = "/v1/public/subscribe/list";
/// 获取用户已订阅套餐(用于判断是否购买过)
static const String kr_getAlreadySubscribe =
"/v1/public/user/subscribe";
/// 获取用户可用订阅(与已订阅接口相同,OmnOem 项目中没有区分)
static const String kr_userAvailableSubscribe =
"/v1/public/user/subscribe";
/// 续费
static const String kr_renewal = "/v1/app/order/renewal";
/// 获取用户订阅流量日志
/// 通过该接口判断订单状态
static const String kr_orderDetail = "/v1/app/order/detail";
/// 获取消息列表
static const String kr_getMessageList = "/v1/public/announcement/list";
/// 获取邀请数据
// static const String kr_getInviteData = "/v1/public/invite/code";
/// 配置信息
static const String kr_config = "/v1/app/auth/config";
/// 获取用户在线时长统计
static const String kr_getUserOnlineTimeStatistics =
"/v1/app/user/online_time/statistics";
/// 获取用户邀请人数
static const String kr_getAffiliateCount = "/v1/public/user/affiliate/count";
/// 获取站点协议
static const String kr_getSiteTos = "/v1/common/site/tos";
/// 隐私政策
static const String kr_getSitePrivacy = "/v1/common/site/privacy";
/// 获取网页文本内容
static const String kr_getWebText = "/v1/common/site/text";
/// 重置订阅周期
static const String kr_resetSubscribePeriod =
"/v1/app/subscribe/reset/period";
/// 获取用户设备列表
static const String kr_getUserDevices = "/v1/public/user/devices";
/// 解绑用户设备
static const String kr_unbindUserDevice = "/v1/public/user/unbind_device";
/// 获取可用支付方式(公开接口)
static const String kr_getPublicPaymentMethods = "/v1/public/payment/methods";
}
+196
View File
@@ -0,0 +1,196 @@
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'dart:io' show Platform;
import '../../model/response/kr_config_data.dart';
import '../../model/response/kr_kr_affiliate_count.dart';
import '../../model/response/kr_message_list.dart';
import '../../model/response/kr_user_info.dart';
import '../../model/response/kr_user_online_duration.dart';
import '../../model/response/kr_web_text.dart';
import '../../network/base_response.dart';
import '../../network/http_error.dart';
import '../../network/http_util.dart';
import 'api.dart';
class KRUserApi {
// 创建一个单例实例
static final KRUserApi _instance = KRUserApi._internal();
factory KRUserApi() => _instance;
// 私有构造函数
KRUserApi._internal();
/// 获取当前系统的 user_agent
String _kr_getUserAgent() {
if (Platform.isAndroid) {
return 'android';
} else if (Platform.isIOS) {
return 'ios';
} else if (Platform.isMacOS) {
return 'mac';
} else if (Platform.isWindows) {
return 'windows';
} else if (Platform.isLinux) {
return 'linux';
} else if (Platform.isFuchsia) {
return 'harmony';
} else {
return 'unknown';
}
}
Future<Either<HttpError, KRMessageList>> kr_getMessageList(
int page, int size, {bool? popup}) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['page'] = page;
data['size'] = size;
if (popup != null) {
data['popup'] = popup;
}
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<KRMessageList>(
Api.kr_getMessageList,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
Future<Either<HttpError, KRUserOnlineDurationResponse>> kr_getUserOnlineTimeStatistics(
) async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRUserOnlineDurationResponse> baseResponse =
await HttpUtil.getInstance().request<KRUserOnlineDurationResponse>(
Api.kr_getUserOnlineTimeStatistics,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
KRLogUtil.kr_i('获取用户在线时长统计: ${baseResponse.model}');
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
// ⚠️ 已废弃:新版本后端不再提供此接口
// Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
// final Map<String, dynamic> data = <String, dynamic>{};
// BaseResponse<KRUserInfo> baseResponse =
// await HttpUtil.getInstance().request<KRUserInfo>(
// Api.kr_getUserInfo,
// data,
// method: HttpMethod.GET,
// isShowLoading: false,
// );
// if (!baseResponse.isSuccess) {
// return left(
// HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
// }
// return right(baseResponse.model);
// }
Future<Either<HttpError, KRAffiliateCount>> kr_getAffiliateCount() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<KRAffiliateCount>(
Api.kr_getAffiliateCount,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
Future<Either<HttpError, KRConfigData>> kr_config() async {
final Map<String, dynamic> data = <String, dynamic>{};
data['user_agent'] = _kr_getUserAgent();
BaseResponse<KRConfigData> baseResponse =
await HttpUtil.getInstance().request<KRConfigData>(
Api.kr_config,
data,
method: HttpMethod.POST,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
/// 获取用户设备列表
Future<Either<HttpError, List<Map<String, dynamic>>>> kr_getUserDevices() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_getUserDevices,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
// 返回设备列表数据
try {
// 响应格式: { data: { list: [...], total: N } }
final responseData = baseResponse.model;
final List<Map<String, dynamic>> devices =
(responseData['list'] as List)
.map((item) => item as Map<String, dynamic>)
.toList();
return right(devices);
} catch (e) {
KRLogUtil.kr_e('解析设备列表失败: $e', tag: 'KRUserApi');
return left(HttpError(msg: '数据解析失败', code: -1));
}
}
/// 解绑用户设备
Future<Either<HttpError, void>> kr_unbindUserDevice(String deviceId) async {
final Map<String, dynamic> data = <String, dynamic>{};
// 将字符串 ID 转换为整数
try {
data['id'] = int.parse(deviceId);
} catch (e) {
KRLogUtil.kr_e('设备ID格式错误: $deviceId', tag: 'KRUserApi');
return left(HttpError(msg: '设备ID格式错误', code: -1));
}
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_unbindUserDevice,
data,
method: HttpMethod.PUT,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(null);
}
}
+333
View File
@@ -0,0 +1,333 @@
import 'dart:io';
import 'dart:math';
import 'dart:convert';
import 'package:fpdart/fpdart.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/mixins/kr_app_bar_opacity_mixin.dart';
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'package:kaer_with_panels/app/model/response/kr_is_register.dart';
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
import 'package:kaer_with_panels/app/network/base_response.dart';
import 'package:kaer_with_panels/app/network/http_error.dart';
import 'package:kaer_with_panels/app/network/http_util.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import '../../utils/kr_common_util.dart';
import '../../utils/kr_log_util.dart';
import '../../utils/kr_aes_util.dart';
import '../kr_device_info_service.dart';
import '../kr_site_config_service.dart';
import '../../common/app_config.dart';
import 'package:dio/dio.dart' as dio;
class KRAuthApi {
/// 检查账号是否已注册(仅支持邮箱)
Future<Either<HttpError, bool>> kr_isRegister(String email) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_isRegister, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.kr_isRegister);
}
/// 注册(仅支持邮箱+密码,验证码和邀请码可选)
Future<Either<HttpError, String>> kr_register(
String email,
String password,
{String? code,
String? inviteCode}) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
data['password'] = password;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
// 验证码是可选的,只有在提供时才发送
if (code != null && code.isNotEmpty) {
data["code"] = code;
}
// 邀请码是可选的
if (inviteCode != null && inviteCode.isNotEmpty) {
data["invite"] = inviteCode;
}
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_register, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.kr_token.toString());
}
/// 验证验证码(仅支持邮箱)
Future<Either<HttpError, bool>> kr_checkVerificationCode(
String email, String code, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
data['code'] = code;
data['type'] = type;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_checkVerificationCode, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
if (baseResponse.model.kr_isRegister) {
return right(true);
} else {
return left(HttpError(msg: "error.70001".tr, code: 70001));
}
}
/// 登录(仅支持邮箱+密码)
Future<Either<HttpError, String>> kr_login(
String email, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
data['password'] = password;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_login, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.kr_token.toString());
}
/// 发送验证码(仅支持邮箱)
/// type: 1=登录, 2=注册, 3=重置密码
Future<Either<HttpError, bool>> kr_sendCode(String email, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
data['type'] = type;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(Api.kr_sendCode, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(true);
}
/// 删除账号
Future<Either<HttpError, String>> kr_deleteAccount(String code) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['code'] = code;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(Api.kr_deleteAccount, data,
method: HttpMethod.DELETE, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right("");
}
/// 忘记密码-设置新密码(仅支持邮箱)
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(
String email, String code, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['email'] = email;
data['password'] = password;
data["code"] = code;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_setNewPsdByForgetPsd, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.kr_token.toString());
}
/// 设备登录(游客登录)
Future<Either<HttpError, String>> kr_deviceLogin() async {
try {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🔐 开始设备登录(游客模式)');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('🔐 开始设备登录(游客模式)', tag: 'KRAuthApi');
// 获取设备信息
final deviceInfoService = KRDeviceInfoService();
final deviceId = deviceInfoService.deviceId;
final userAgent = deviceInfoService.getUserAgent();
if (deviceId == null) {
print('❌ 设备ID为空,无法登录');
return left(HttpError(msg: '设备ID获取失败', code: -1));
}
print('📱 设备ID: $deviceId');
print('📱 User-Agent: $userAgent');
KRLogUtil.kr_i('📱 设备ID: $deviceId', tag: 'KRAuthApi');
KRLogUtil.kr_i('📱 User-Agent: $userAgent', tag: 'KRAuthApi');
// 构建请求数据
Map<String, dynamic> data = {
'identifier': deviceId,
'user_agent': userAgent,
};
print('📤 原始请求数据: $data');
// 检查是否需要加密
final siteConfigService = KRSiteConfigService();
final needEncryption = siteConfigService.isDeviceSecurityEnabled();
print('🔒 是否需要加密: $needEncryption');
KRLogUtil.kr_i('🔒 是否需要加密: $needEncryption', tag: 'KRAuthApi');
String? requestBody;
if (needEncryption) {
// 加密请求数据
print('🔐 加密请求数据...');
final encrypted = KRAesUtil.encryptJson(data, AppConfig.kr_encryptionKey);
requestBody = '{"data":"${encrypted['data']}","time":"${encrypted['time']}"}';
print('🔐 加密后请求体: $requestBody');
KRLogUtil.kr_i('🔐 加密后请求体', tag: 'KRAuthApi');
} else {
// 使用明文
requestBody = jsonEncode(data);
print('📝 明文请求体: $requestBody');
}
// 使用 Dio 直接发送请求(因为需要特殊的加密处理)
final dioInstance = dio.Dio();
final baseUrl = AppConfig.getInstance().baseUrl;
final url = '$baseUrl${Api.kr_deviceLogin}';
print('📤 请求URL: $url');
KRLogUtil.kr_i('📤 请求URL: $url', tag: 'KRAuthApi');
// 设置请求头
final headers = <String, String>{
'Content-Type': 'application/json',
};
if (needEncryption) {
headers['Login-Type'] = 'device';
}
print('📤 请求头: $headers');
// 配置Dio实例的超时设置
dioInstance.options.connectTimeout = const Duration(seconds: 10);
dioInstance.options.sendTimeout = const Duration(seconds: 10);
dioInstance.options.receiveTimeout = const Duration(seconds: 10);
final response = await dioInstance.post(
url,
data: requestBody,
options: dio.Options(
headers: headers,
),
);
print('📥 响应状态码: ${response.statusCode}');
print('📥 响应数据: ${response.data}');
KRLogUtil.kr_i('📥 响应状态码: ${response.statusCode}', tag: 'KRAuthApi');
KRLogUtil.kr_i('📥 响应数据: ${response.data}', tag: 'KRAuthApi');
if (response.statusCode == 200) {
Map<String, dynamic> responseData = response.data as Map<String, dynamic>;
// 检查是否需要解密响应
if (needEncryption && responseData.containsKey('data')) {
final dataField = responseData['data'];
if (dataField is Map<String, dynamic> &&
dataField.containsKey('data') &&
dataField.containsKey('time')) {
print('🔓 解密响应数据...');
final decrypted = KRAesUtil.decryptJson(
dataField['data'] as String,
dataField['time'] as String,
AppConfig.kr_encryptionKey,
);
responseData['data'] = decrypted;
print('🔓 解密后数据: ${responseData['data']}');
KRLogUtil.kr_i('🔓 解密成功', tag: 'KRAuthApi');
}
}
if (responseData['code'] == 200) {
final token = responseData['data']['token'] as String;
print('✅ 设备登录成功');
print('🎫 Token: ${token.substring(0, min(20, token.length))}...');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('✅ 设备登录成功', tag: 'KRAuthApi');
return right(token);
} else {
final msg = responseData['msg'] ?? '登录失败';
print('❌ 登录失败: $msg');
return left(HttpError(msg: msg, code: responseData['code']));
}
} else {
print('❌ HTTP错误: ${response.statusCode}');
return left(HttpError(msg: 'HTTP错误', code: response.statusCode ?? -1));
}
} on dio.DioException catch (e) {
print('❌ Dio异常: ${e.type}');
print('❌ 错误信息: ${e.message}');
KRLogUtil.kr_e('❌ 设备登录Dio异常: ${e.message}', tag: 'KRAuthApi');
return left(HttpError(msg: '网络请求失败: ${e.message}', code: -1));
} catch (e, stackTrace) {
print('❌ 设备登录异常: $e');
print('📚 堆栈跟踪: $stackTrace');
KRLogUtil.kr_e('❌ 设备登录异常: $e', tag: 'KRAuthApi');
return left(HttpError(msg: '设备登录失败: $e', code: -1));
}
}
String _kr_getUserAgent() {
if (Platform.isAndroid) {
return 'android';
} else if (Platform.isIOS) {
return 'ios';
} else if (Platform.isMacOS) {
return 'mac';
} else if (Platform.isWindows) {
return 'windows';
} else if (Platform.isLinux) {
return 'linux';
} else if (Platform.isFuchsia) {
return 'harmony';
} else {
return 'unknown';
}
}
}
+293
View File
@@ -0,0 +1,293 @@
import 'dart:ffi';
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/model/response/kr_login_data.dart';
import 'package:kaer_with_panels/app/model/response/kr_node_list.dart';
import 'package:kaer_with_panels/app/model/response/kr_package_list.dart';
import 'package:kaer_with_panels/app/network/base_response.dart';
import 'package:kaer_with_panels/app/network/http_error.dart';
import 'package:kaer_with_panels/app/network/http_util.dart';
import '../../model/response/kr_already_subscribe.dart';
import '../../model/response/kr_node_group_list.dart';
import '../../model/response/kr_order_status.dart';
import '../../model/response/kr_payment_methods.dart';
import '../../model/response/kr_purchase_order_no.dart';
import '../../model/response/kr_status.dart';
import '../../model/response/kr_user_available_subscribe.dart';
/// 订阅相关
class KRSubscribeApi {
/// 获取可购买套餐
Future<Either<HttpError, KRPackageList>> kr_getPackageListList() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRPackageList> baseResponse =
await HttpUtil.getInstance().request<KRPackageList>(
Api.kr_getPackageList,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
/// 获取节点列表
Future<Either<HttpError, KRNodeList>> kr_nodeList(int id) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
BaseResponse<KRNodeList> baseResponse =
await HttpUtil.getInstance().request<KRNodeList>(
Api.kr_nodeList,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
/// 获取用户可用订阅
Future<Either<HttpError, List<KRUserAvailableSubscribeItem>>>
kr_userAvailableSubscribe({bool containsNodes = false}) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['contains_nodes'] = containsNodes;
BaseResponse<KRUserAvailableSubscribeList> baseResponse =
await HttpUtil.getInstance().request<KRUserAvailableSubscribeList>(
Api.kr_userAvailableSubscribe,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.list);
}
/// 获取分组节点
Future<Either<HttpError, List<KRNodeGroupListItem>>>
kr_nodeGroupList() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRNodeGroupList> baseResponse =
await HttpUtil.getInstance().request<KRNodeGroupList>(
Api.kr_nodeGroupList,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.list);
}
/// 通过该接口判断订单状态
Future<Either<HttpError, KROrderStatus>> kr_orderDetail(
String orderId) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['order_no'] = orderId;
BaseResponse<KROrderStatus> baseResponse =
await HttpUtil.getInstance().request<KROrderStatus>(
Api.kr_orderDetail,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
/// 获取支付方式
Future<Either<HttpError, List<KRPaymentMethod>>>
kr_getPaymentMethods() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRPaymentMethods> baseResponse =
await HttpUtil.getInstance().request<KRPaymentMethods>(
Api.kr_getPaymentMethods,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.list);
}
/// 进行下单
Future<Either<HttpError, String>> kr_purchase(
int planId, int quantity, int payment, String coupon) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['subscribe_id'] = planId;
data['quantity'] = quantity;
data['payment'] = payment;
data['coupon'] = "";
BaseResponse<KRPurchaseOrderNo> baseResponse =
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
Api.kr_purchase,
data,
method: HttpMethod.POST,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.orderNo);
}
/// 续费
Future<Either<HttpError, String>> kr_renewal(
int planId, int quantity, int payment, String coupon) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['user_subscribe_id'] = planId;
data['quantity'] = quantity;
data['payment'] = payment;
data['coupon'] = "";
BaseResponse<KRPurchaseOrderNo> baseResponse =
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
Api.kr_renewal,
data,
method: HttpMethod.POST,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.orderNo);
}
/// 获取用户已订阅套餐
Future<Either<HttpError, List<KRAlreadySubscribe>>>
kr_getAlreadySubscribe() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRAlreadySubscribeList> baseResponse =
await HttpUtil.getInstance().request<KRAlreadySubscribeList>(
Api.kr_getAlreadySubscribe,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.list);
}
Future<Either<HttpError, String>> kr_prePurchase(
int planId, int quantity, String payment, String coupon) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['subscribe_id'] = planId;
data['quantity'] = quantity;
data['payment'] = payment;
data['coupon'] = "";
BaseResponse<KRPurchaseOrderNo> baseResponse =
await HttpUtil.getInstance().request<KRPurchaseOrderNo>(
Api.kr_preOrder,
data,
method: HttpMethod.POST,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.orderNo);
}
/// 获取支付地址,跳转到付款地址
Future<Either<HttpError, String>> kr_checkout(String orderId) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['orderNo'] = orderId;
data['returnUrl'] = AppConfig.getInstance().baseUrl;
BaseResponse<KRPurchaseOrderUrl> baseResponse =
await HttpUtil.getInstance().request<KRPurchaseOrderUrl>(
Api.kr_checkout,
data,
method: HttpMethod.POST,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.url);
}
/// 重置订阅周期
Future<Either<HttpError, bool>> kr_resetSubscribePeriod(
int userSubscribeId) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['user_subscribe_id'] = userSubscribeId;
BaseResponse<KRStatus> baseResponse =
await HttpUtil.getInstance().request<KRStatus>(
Api.kr_resetSubscribePeriod,
data,
method: HttpMethod.POST,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model.kr_bl);
}
/// 获取公开的支付方式列表
Future<Either<HttpError, dynamic>> kr_getPublicPaymentMethods() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_getPublicPaymentMethods,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
}
+56
View File
@@ -0,0 +1,56 @@
import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/model/response/kr_web_text.dart';
import 'package:kaer_with_panels/app/network/base_response.dart';
import 'package:kaer_with_panels/app/network/http_error.dart';
import 'package:kaer_with_panels/app/network/http_util.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
/// 网页相关 API
class KRWebApi {
/// 获取网页文本内容
Future<Either<HttpError, String>> kr_getWebText(String url) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['url'] = url;
BaseResponse<KRWebText> baseResponse = await HttpUtil.getInstance().request<KRWebText>(
url,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
// 根据 URL 返回对应的内容
if (url.contains(Api.kr_getSitePrivacy)) {
return right(baseResponse.model.privacyPolicy);
} else if (url.contains(Api.kr_getSiteTos)) {
return right(baseResponse.model.tosContent);
} else {
return right(baseResponse.model.privacyPolicy); // 默认返回隐私政策
}
}
/// 获取网页内容
// Future<Either<String, String>> kr_getWebContent() async {
// try {
// // ... 其他代码 ...
// } catch (e) {
// KRLogUtil.kr_e('获取网页内容失败: $e', tag: 'WebApi');
// return Left('获取网页内容失败: $e');
// }
// }
// /// 获取网页内容
// Future<Either<String, String>> kr_getWebContentWithRetry() async {
// try {
// // ... 其他代码 ...
// } catch (e) {
// KRLogUtil.kr_e('获取网页内容失败: $e', tag: 'WebApi');
// return Left('获取网页内容失败: $e');
// }
// }
}
+153
View File
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../model/response/kr_message_list.dart';
import 'api_service/kr_api.user.dart';
import '../utils/kr_common_util.dart';
import '../widgets/dialogs/kr_dialog.dart';
import '../localization/app_translations.dart';
class KRAnnouncementService {
static final KRAnnouncementService _instance = KRAnnouncementService._internal();
final KRUserApi _kr_userApi = KRUserApi();
bool _kr_hasShownAnnouncement = false;
factory KRAnnouncementService() {
return _instance;
}
KRAnnouncementService._internal();
// 重置公告显示状态(用于退出登录时)
void kr_reset() {
_kr_hasShownAnnouncement = false;
}
// 检查是否需要显示公告弹窗
Future<void> kr_checkAnnouncement() async {
if (_kr_hasShownAnnouncement) {
return;
}
final either = await _kr_userApi.kr_getMessageList(1, 1, popup: true);
either.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
},
(list) {
if (list.announcements.isNotEmpty) {
// 按创建时间降序排序,获取最新的公告
final sortedAnnouncements = list.announcements;
final latestAnnouncement = sortedAnnouncements.first;
// 如果需要弹窗显示
if (latestAnnouncement.popup) {
_kr_hasShownAnnouncement = true;
KRDialog.show(
title: latestAnnouncement.title,
message: null,
confirmText: AppTranslations.kr_dialog.kr_iKnow,
onConfirm: () {
// Navigator.of(Get.context!).pop();
},
customMessageWidget: _kr_buildMessageContent(latestAnnouncement.content, Get.context!),
);
}
}
},
);
}
// 构建消息内容
Widget _kr_buildMessageContent(String content, BuildContext context) {
// 判断内容类型
final bool kr_isHtml = content.contains('<') && content.contains('>');
final bool kr_isMarkdown = content.contains('**') ||
content.contains('*') ||
content.contains('#') ||
content.contains('- ') ||
content.contains('[');
final textStyle = TextStyle(
fontSize: 14.sp,
color: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
);
if (kr_isHtml) {
// 使用 flutter_html 处理 HTML 内容
return Html(
data: content,
style: {
'body': Style(
margin: Margins.all(0),
padding: HtmlPaddings.all(0),
fontSize: FontSize(14.sp),
color: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
lineHeight: LineHeight(1.4),
),
'p': Style(
margin: Margins.only(bottom: 8.h),
),
'b': Style(
fontWeight: FontWeight.bold,
),
'i': Style(
fontStyle: FontStyle.italic,
),
'a': Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
),
},
shrinkWrap: true,
);
} else if (kr_isMarkdown) {
// 使用 flutter_markdown 处理 Markdown 内容
return MarkdownBody(
data: content,
styleSheet: MarkdownStyleSheet(
p: TextStyle(
fontSize: 14.sp,
color: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
strong: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
em: TextStyle(
fontSize: 14.sp,
fontStyle: FontStyle.italic,
color: Theme.of(context).textTheme.bodySmall?.color,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
a: TextStyle(
fontSize: 14.sp,
color: Colors.blue,
decoration: TextDecoration.underline,
fontFamily: 'AlibabaPuHuiTi-Regular',
height: 1.4,
),
),
);
} else {
// 普通文本
return Text(
content,
style: textStyle,
);
}
}
}
@@ -0,0 +1,260 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import '../utils/kr_secure_storage.dart';
import '../utils/kr_log_util.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';
/// 设备信息服务
/// 用于获取设备唯一标识和其他设备信息
class KRDeviceInfoService {
static final KRDeviceInfoService _instance = KRDeviceInfoService._internal();
factory KRDeviceInfoService() => _instance;
KRDeviceInfoService._internal();
final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
String? _deviceId;
Map<String, dynamic>? _deviceDetails;
// 获取设备唯一标识
String? get deviceId => _deviceId;
// 获取设备详细信息
Map<String, dynamic>? get deviceDetails => _deviceDetails;
/// 初始化设备信息
Future<void> initialize() async {
try {
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('📱 开始初始化设备信息服务');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('📱 开始初始化设备信息', tag: 'KRDeviceInfoService');
_deviceId = await _getDeviceId();
_deviceDetails = await _getDeviceDetails();
print('✅ 设备信息初始化成功');
print('📱 设备ID: $_deviceId');
print('📱 设备平台: ${getPlatformName()}');
print('📱 设备型号: ${getDeviceModel()}');
print('📱 系统版本: ${getOSVersion()}');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('✅ 设备信息初始化成功', tag: 'KRDeviceInfoService');
KRLogUtil.kr_i('📱 设备ID - $_deviceId', tag: 'KRDeviceInfoService');
KRLogUtil.kr_i('📱 设备详情 - $_deviceDetails', tag: 'KRDeviceInfoService');
} catch (e) {
print('❌ 设备信息初始化失败: $e');
KRLogUtil.kr_e('❌ 设备信息初始化失败 - $e', tag: 'KRDeviceInfoService');
}
}
/// 获取设备唯一标识
Future<String> _getDeviceId() async {
try {
String? identifier;
if (Platform.isAndroid) {
final androidInfo = await _deviceInfo.androidInfo;
// Android使用androidId作为唯一标识
identifier = androidInfo.id;
} else if (Platform.isIOS) {
final iosInfo = await _deviceInfo.iosInfo;
// iOS使用identifierForVendor作为唯一标识
identifier = iosInfo.identifierForVendor;
} else if (Platform.isMacOS) {
final macInfo = await _deviceInfo.macOsInfo;
// macOS使用systemGUID
identifier = macInfo.systemGUID;
} else if (Platform.isWindows) {
final windowsInfo = await _deviceInfo.windowsInfo;
// Windows使用计算机名作为唯一标识
identifier = windowsInfo.computerName;
} else if (Platform.isLinux) {
final linuxInfo = await _deviceInfo.linuxInfo;
// Linux使用machineId
identifier = linuxInfo.machineId;
} else {
// Web或其他平台,使用生成的UUID
identifier = await _getOrCreateStoredDeviceId();
}
// 如果获取失败,使用存储的或生成新的ID
if (identifier == null || identifier.isEmpty) {
identifier = await _getOrCreateStoredDeviceId();
}
return identifier;
} catch (e) {
print('❌ 获取设备ID失败: $e');
KRLogUtil.kr_e('❌ 获取设备ID失败 - $e', tag: 'KRDeviceInfoService');
// 如果获取失败,返回存储的或生成新的ID
return await _getOrCreateStoredDeviceId();
}
}
/// 获取或创建存储的设备ID
Future<String> _getOrCreateStoredDeviceId() async {
try {
const key = 'kr_device_unique_id';
final storage = KRSecureStorage();
String? storedId = await storage.kr_readData(key: key);
if (storedId == null || storedId.isEmpty) {
// 生成新的UUID
storedId = _generateUniqueId();
await storage.kr_saveData(key: key, value: storedId);
print('📱 生成新的设备ID: $storedId');
KRLogUtil.kr_i('📱 生成新的设备ID - $storedId', tag: 'KRDeviceInfoService');
} else {
print('📱 使用存储的设备ID: $storedId');
KRLogUtil.kr_i('📱 使用存储的设备ID - $storedId', tag: 'KRDeviceInfoService');
}
return storedId;
} catch (e) {
print('❌ 获取存储的设备ID失败: $e');
KRLogUtil.kr_e('❌ 获取存储的设备ID失败 - $e', tag: 'KRDeviceInfoService');
return _generateUniqueId();
}
}
/// 生成唯一ID
String _generateUniqueId() {
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final random = DateTime.now().microsecondsSinceEpoch.toString();
final combined = '$timestamp-$random';
// 使用MD5生成唯一标识
final bytes = utf8.encode(combined);
final digest = md5.convert(bytes);
return digest.toString();
}
/// 获取设备详细信息
Future<Map<String, dynamic>> _getDeviceDetails() async {
try {
if (Platform.isAndroid) {
final androidInfo = await _deviceInfo.androidInfo;
return {
'platform': 'android',
'device': androidInfo.device,
'model': androidInfo.model,
'brand': androidInfo.brand,
'manufacturer': androidInfo.manufacturer,
'androidId': androidInfo.id,
'version': androidInfo.version.release,
'sdkInt': androidInfo.version.sdkInt,
};
} else if (Platform.isIOS) {
final iosInfo = await _deviceInfo.iosInfo;
return {
'platform': 'ios',
'name': iosInfo.name,
'model': iosInfo.model,
'systemName': iosInfo.systemName,
'systemVersion': iosInfo.systemVersion,
'identifierForVendor': iosInfo.identifierForVendor,
'isPhysicalDevice': iosInfo.isPhysicalDevice,
};
} else if (Platform.isMacOS) {
final macInfo = await _deviceInfo.macOsInfo;
return {
'platform': 'macos',
'computerName': macInfo.computerName,
'model': macInfo.model,
'hostName': macInfo.hostName,
'arch': macInfo.arch,
'systemGUID': macInfo.systemGUID,
};
} else if (Platform.isWindows) {
final windowsInfo = await _deviceInfo.windowsInfo;
return {
'platform': 'windows',
'computerName': windowsInfo.computerName,
'numberOfCores': windowsInfo.numberOfCores,
'systemMemoryInMegabytes': windowsInfo.systemMemoryInMegabytes,
};
} else if (Platform.isLinux) {
final linuxInfo = await _deviceInfo.linuxInfo;
return {
'platform': 'linux',
'name': linuxInfo.name,
'version': linuxInfo.version,
'id': linuxInfo.id,
'machineId': linuxInfo.machineId,
};
} else if (kIsWeb) {
final webInfo = await _deviceInfo.webBrowserInfo;
return {
'platform': 'web',
'browserName': webInfo.browserName.toString(),
'userAgent': webInfo.userAgent,
'vendor': webInfo.vendor,
};
}
return {
'platform': 'unknown',
};
} catch (e) {
print('❌ 获取设备详情失败: $e');
KRLogUtil.kr_e('❌ 获取设备详情失败 - $e', tag: 'KRDeviceInfoService');
return {
'platform': 'unknown',
'error': e.toString(),
};
}
}
/// 获取平台名称
String getPlatformName() {
if (Platform.isAndroid) return 'Android';
if (Platform.isIOS) return 'iOS';
if (Platform.isMacOS) return 'macOS';
if (Platform.isWindows) return 'Windows';
if (Platform.isLinux) return 'Linux';
if (kIsWeb) return 'Web';
return 'Unknown';
}
/// 获取设备型号
String getDeviceModel() {
if (_deviceDetails == null) return 'Unknown';
if (Platform.isAndroid) {
return '${_deviceDetails!['brand']} ${_deviceDetails!['model']}';
} else if (Platform.isIOS) {
return _deviceDetails!['model'] ?? 'Unknown';
} else if (Platform.isMacOS) {
return _deviceDetails!['model'] ?? 'Unknown';
}
return 'Unknown';
}
/// 获取操作系统版本
String getOSVersion() {
if (_deviceDetails == null) return 'Unknown';
if (Platform.isAndroid) {
return _deviceDetails!['version'] ?? 'Unknown';
} else if (Platform.isIOS) {
return _deviceDetails!['systemVersion'] ?? 'Unknown';
}
return 'Unknown';
}
/// 获取User-Agent信息
String getUserAgent() {
final platform = getPlatformName();
final model = getDeviceModel();
final osVersion = getOSVersion();
return 'BearVPN/1.0.0 ($platform; $model; $osVersion) Flutter';
}
}
@@ -0,0 +1,292 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../model/response/kr_site_config.dart';
import '../common/app_config.dart';
import '../utils/kr_log_util.dart';
/// 网站配置服务
class KRSiteConfigService extends ChangeNotifier {
static final KRSiteConfigService _instance = KRSiteConfigService._internal();
factory KRSiteConfigService() => _instance;
KRSiteConfigService._internal() {
// 配置 Dio 默认超时设置
_dio.options.connectTimeout = const Duration(seconds: 10);
_dio.options.sendTimeout = const Duration(seconds: 10);
_dio.options.receiveTimeout = const Duration(seconds: 10);
}
KRSiteConfig? _siteConfig;
bool _isInitialized = false;
final Dio _dio = Dio();
/// 获取站点配置
KRSiteConfig? get siteConfig => _siteConfig;
/// 是否已初始化
bool get isInitialized => _isInitialized;
/// 初始化站点配置
Future<bool> initialize() async {
try {
print('🔧 KRSiteConfigService.initialize() 开始执行');
KRLogUtil.kr_i('🔧 开始初始化网站配置', tag: 'KRSiteConfigService');
// Debug 模式下使用固定地址
final baseUrl = AppConfig().baseUrl;
print('📍 baseUrl = $baseUrl');
final url = '$baseUrl/v1/common/site/config';
print('📍 完整URL = $url');
KRLogUtil.kr_i('📤 请求网站配置 - $url', tag: 'KRSiteConfigService');
print('📤 准备发送 GET 请求到: $url');
print('⏱️ 超时配置: connectTimeout=10s, sendTimeout=10s, receiveTimeout=10s');
print('⏳ 开始发送请求...');
final startTime = DateTime.now();
final response = await _dio.get(url);
final endTime = DateTime.now();
final duration = endTime.difference(startTime).inMilliseconds;
print('⏱️ 请求耗时: ${duration}ms');
print('✅ 请求完成,状态码: ${response.statusCode}');
KRLogUtil.kr_i('📥 响应状态码 - ${response.statusCode}', tag: 'KRSiteConfigService');
if (response.statusCode == 200) {
final responseData = response.data;
print('📥 响应数据类型: ${responseData.runtimeType}');
print('📥 响应数据: $responseData');
KRLogUtil.kr_i('📥 响应数据 - $responseData', tag: 'KRSiteConfigService');
if (responseData['code'] == 200) {
_siteConfig = KRSiteConfig.fromJson(responseData['data']);
_isInitialized = true;
// 打印配置信息
_printConfigInfo();
// 通知监听者配置已更新
notifyListeners();
return true;
} else {
KRLogUtil.kr_e('❌ API返回错误 - ${responseData['msg']}', tag: 'KRSiteConfigService');
return false;
}
} else {
KRLogUtil.kr_e('❌ HTTP错误 - ${response.statusCode}', tag: 'KRSiteConfigService');
return false;
}
} on DioException catch (e, stackTrace) {
print('❌ Dio请求异常: ${e.type}');
print('❌ 错误信息: ${e.message}');
print('❌ 请求URL: ${e.requestOptions.uri}');
print('❌ 连接超时: ${e.requestOptions.connectTimeout}');
print('❌ 发送超时: ${e.requestOptions.sendTimeout}');
print('❌ 接收超时: ${e.requestOptions.receiveTimeout}');
if (e.response != null) {
print('❌ 响应状态码: ${e.response?.statusCode}');
print('❌ 响应数据: ${e.response?.data}');
}
print('📚 堆栈跟踪: $stackTrace');
KRLogUtil.kr_e('❌ Dio异常 - ${e.type}: ${e.message}', tag: 'KRSiteConfigService');
KRLogUtil.kr_e('📚 堆栈: $stackTrace', tag: 'KRSiteConfigService');
return false;
} catch (e, stackTrace) {
print('❌ 未知异常: $e');
print('📚 堆栈跟踪: $stackTrace');
KRLogUtil.kr_e('❌ 初始化失败 - $e', tag: 'KRSiteConfigService');
KRLogUtil.kr_e('📚 堆栈: $stackTrace', tag: 'KRSiteConfigService');
return false;
}
}
/// 打印配置信息
void _printConfigInfo() {
if (_siteConfig == null) return;
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('📊 网站配置信息:', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
// 站点信息
KRLogUtil.kr_i('🏠 站点名称: ${_siteConfig!.site.siteName}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('🏠 站点描述: ${_siteConfig!.site.siteDesc}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('🏠 站点域名: ${_siteConfig!.site.host}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('💬 Crisp ID: ${_siteConfig!.site.crispId}', tag: 'KRSiteConfigService');
// 注册相关
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('📝 注册配置:', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 开放注册: ${isRegisterEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 手机号注册: ${isMobileRegisterEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 邮箱注册: ${isEmailRegisterEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 设备登录: ${isDeviceLoginEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
// 验证相关
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('🔐 验证配置:', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 邮箱验证: ${isEmailVerificationEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 手机验证: ${isMobileVerificationEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 登录验证: ${isLoginVerificationEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 注册验证: ${isRegisterVerificationEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 重置密码验证: ${isResetPasswordVerificationEnabled() ? "" : ""}', tag: 'KRSiteConfigService');
// 邀请相关
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('🎁 邀请配置:', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 强制邀请码: ${isForcedInvite() ? "" : ""}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 推荐比例: ${_siteConfig!.invite.referralPercentage}%', tag: 'KRSiteConfigService');
// 货币相关
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('💰 货币配置:', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 货币单位: ${_siteConfig!.currency.currencyUnit}', tag: 'KRSiteConfigService');
KRLogUtil.kr_i(' ✓ 货币符号: ${_siteConfig!.currency.currencySymbol}', tag: 'KRSiteConfigService');
// OAuth 方法
if (_siteConfig!.oauthMethods.isNotEmpty) {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('🔑 OAuth 方法: ${_siteConfig!.oauthMethods.join(", ")}', tag: 'KRSiteConfigService');
}
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('✅ 网站配置初始化成功', tag: 'KRSiteConfigService');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'KRSiteConfigService');
}
/// 是否开启手机号注册
bool isMobileRegisterEnabled() {
return _siteConfig?.auth.mobile.enable ?? false;
}
/// 是否开启邮箱注册
bool isEmailRegisterEnabled() {
return _siteConfig?.auth.email.enable ?? false;
}
/// 是否开放注册(未停止注册)
bool isRegisterEnabled() {
return !(_siteConfig?.auth.register.stopRegister ?? true);
}
/// 是否开启邮箱验证
bool isEmailVerificationEnabled() {
return _siteConfig?.auth.email.enableVerify ?? false;
}
/// 是否开启手机验证
bool isMobileVerificationEnabled() {
return _siteConfig?.auth.mobile.enable ?? false;
}
/// 是否开启登录验证
bool isLoginVerificationEnabled() {
return _siteConfig?.verify.enableLoginVerify ?? false;
}
/// 是否开启注册验证
bool isRegisterVerificationEnabled() {
return _siteConfig?.verify.enableRegisterVerify ?? false;
}
/// 是否开启重置密码验证
bool isResetPasswordVerificationEnabled() {
return _siteConfig?.verify.enableResetPasswordVerify ?? false;
}
/// 是否强制邀请码
bool isForcedInvite() {
return _siteConfig?.invite.forcedInvite ?? false;
}
/// 获取验证码间隔时间(秒)
int getVerifyCodeInterval() {
return _siteConfig?.verifyCode.verifyCodeInterval ?? 60;
}
/// 获取OAuth方法列表
List<String> getOAuthMethods() {
return _siteConfig?.oauthMethods ?? [];
}
/// 检查是否支持设备模式(匿名游客模式)
bool isDeviceModeSupported() {
final oauthMethods = getOAuthMethods();
return oauthMethods.contains('device');
}
/// 检查是否启用设备登录
bool isDeviceLoginEnabled() {
return _siteConfig?.auth.device.enable ?? false;
}
/// 检查是否需要设备安全加密
bool isDeviceSecurityEnabled() {
return _siteConfig?.auth.device.enableSecurity ?? false;
}
/// 检查是否显示广告
bool isDeviceShowAds() {
return _siteConfig?.auth.device.showAds ?? false;
}
/// 检查是否只允许真实设备
bool isOnlyRealDevice() {
return _siteConfig?.auth.device.onlyRealDevice ?? false;
}
/// 获取站点信息
KRSiteInfo? getSiteInfo() {
return _siteConfig?.site;
}
/// 获取货币配置
KRCurrencyConfig? getCurrencyConfig() {
return _siteConfig?.currency;
}
/// 获取订阅配置
KRSubscribeConfig? getSubscribeConfig() {
return _siteConfig?.subscribe;
}
/// 检查手机号是否在白名单中
bool isMobileInWhitelist(String mobile) {
if (!(_siteConfig?.auth.mobile.enableWhitelist ?? false)) {
return true; // 如果未开启白名单,则允许所有手机号
}
final whitelist = _siteConfig?.auth.mobile.whitelist ?? [];
return whitelist.contains(mobile);
}
/// 检查邮箱域名是否被允许
bool isEmailDomainAllowed(String email) {
if (!(_siteConfig?.auth.email.enableDomainSuffix ?? false)) {
return true; // 如果未开启域名限制,则允许所有域名
}
final domainSuffixList = _siteConfig?.auth.email.domainSuffixList ?? '';
if (domainSuffixList.isEmpty) {
return true;
}
final allowedDomains = domainSuffixList.split(',').map((d) => d.trim()).toList();
final emailDomain = email.split('@').last.toLowerCase();
return allowedDomains.any((domain) => emailDomain.endsWith(domain.toLowerCase()));
}
/// 获取Crisp客服系统ID
String getCrispId() {
return _siteConfig?.site.crispId ?? '0';
}
/// 重置配置
void reset() {
_siteConfig = null;
_isInitialized = false;
notifyListeners();
}
}
+311
View File
@@ -0,0 +1,311 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
/// WebSocket 服务类
/// 用于处理与服务器的 WebSocket 连接、心跳和消息处理
class KrSocketService {
// 单例实例
static final KrSocketService _instance = KrSocketService._internal();
// 私有变量
WebSocket? _socket;
StreamSubscription? _socketSubscription; // 添加订阅管理
Timer? _heartbeatTimer;
Timer? _heartbeatTimeoutTimer;
int _heartbeatTimeoutCount = 0;
Timer? _reconnectTimer;
Timer? _vpnStateChangeTimer;
String? _baseUrl;
String? _userId;
String? _deviceNumber;
String? _token;
// 消息处理回调
Function(Map<String, dynamic>)? _onMessageCallback;
// 连接状态回调
Function(bool)? _onConnectionStateCallback;
int _reconnectAttempts = 0;
// 连接状态
bool _isConnecting = false;
bool _isConnected = false;
// 连接状态检查
bool _isConnectionStable = false;
Timer? _connectionStabilityTimer;
static const Duration _connectionStabilityTimeout = Duration(seconds: 10);
// 心跳相关
static const int _maxHeartbeatTimeout = 3; // 最大心跳超时次数
static const Duration _heartbeatTimeout = Duration(seconds: 10); // 心跳响应超时时间
// 私有构造函数
KrSocketService._internal();
// 工厂构造函数
factory KrSocketService() => _instance;
// 获取实例
static KrSocketService get instance => _instance;
/// 初始化 WebSocket 服务
void kr_init({
required String baseUrl,
required String userId,
required String deviceNumber,
required String token,
}) {
_baseUrl = baseUrl;
_userId = userId;
_deviceNumber = deviceNumber;
_token = token;
}
/// 设置消息处理回调
void setOnMessageCallback(Function(Map<String, dynamic>) callback) {
_onMessageCallback = callback;
}
/// 设置连接状态回调
void setOnConnectionStateCallback(Function(bool) callback) {
_onConnectionStateCallback = callback;
}
/// 连接到 WebSocket 服务器
Future<void> connect() async {
if (_isConnecting || _isConnected) {
KRLogUtil.kr_i('WebSocket 正在连接或已连接,跳过重复连接', tag: 'WebSocket');
return;
}
_isConnecting = true;
KRLogUtil.kr_i('开始连接 WebSocket...', tag: 'WebSocket');
try {
// 确保 URL 使用 ws:// 或 wss:// 协议
final uri = Uri.parse(_baseUrl!.startsWith('http')
? _baseUrl!.replaceFirst('http', 'ws')
: _baseUrl!);
// 构建 WebSocket URL,确保格式正确
final wsUrl = Uri(
scheme: uri.scheme,
host: uri.host,
port: uri.port,
path: '/v1/app/ws/$_userId/$_deviceNumber',
).toString();
KRLogUtil.kr_i('连接地址: $wsUrl', tag: 'WebSocket');
// 清理旧的连接
_cleanup();
// 创建 WebSocket 连接
_socket = await WebSocket.connect(
wsUrl,
headers: {
'Authorization': _token!,
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
},
);
// 设置消息监听并保存订阅
_socketSubscription = _socket!.listen(
(message) {
KRLogUtil.kr_i('收到消息: $message', tag: 'WebSocket');
_handleMessage(message);
},
onError: (error) {
KRLogUtil.kr_e('WebSocket 错误: $error', tag: 'WebSocket');
_handleConnectionError();
},
onDone: () {
KRLogUtil.kr_i('WebSocket 连接关闭', tag: 'WebSocket');
_handleConnectionError();
},
);
KRLogUtil.kr_i('WebSocket 连接成功', tag: 'WebSocket');
_isConnected = true;
_isConnecting = false;
_reconnectAttempts = 0;
// 等待一小段时间后再发送心跳
await Future.delayed(const Duration(seconds: 1));
// 开始心跳
_startHeartbeat();
_onConnectionStateCallback?.call(true);
} catch (e, stackTrace) {
KRLogUtil.kr_e('WebSocket 连接失败: $e', tag: 'WebSocket');
KRLogUtil.kr_e('错误堆栈: $stackTrace', tag: 'WebSocket');
_isConnecting = false;
_handleConnectionError();
}
}
/// 处理连接错误
void _handleConnectionError() {
_cleanup();
// 检查是否已登录
if (!KRAppRunData.getInstance().kr_isLogin.value) {
KRLogUtil.kr_i('用户已退出登录,停止重连', tag: 'WebSocket');
return;
}
_reconnectAttempts++;
// 使用固定 5 秒的重连间隔
const backoffDelay = Duration(seconds: 5);
KRLogUtil.kr_i('尝试重连 (第 $_reconnectAttempts 次, 间隔: ${backoffDelay.inSeconds}秒)...', tag: 'WebSocket');
_reconnectTimer?.cancel();
_reconnectTimer = Timer(backoffDelay, () {
connect();
});
}
/// 开始心跳
void _startHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimeoutTimer?.cancel();
_heartbeatTimeoutCount = 0;
// 确保连接成功后再发送心跳
if (_isConnected) {
KRLogUtil.kr_i('发送初始心跳...', tag: 'WebSocket');
sendMessage('ping');
_heartbeatTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
if (_isConnected) {
KRLogUtil.kr_i('发送心跳...', tag: 'WebSocket');
sendMessage('ping');
// 启动心跳响应超时检测
_heartbeatTimeoutTimer?.cancel();
_heartbeatTimeoutTimer = Timer(_heartbeatTimeout, () {
_heartbeatTimeoutCount++;
KRLogUtil.kr_w('心跳响应超时 (第 $_heartbeatTimeoutCount 次)', tag: 'WebSocket');
if (_heartbeatTimeoutCount >= _maxHeartbeatTimeout) {
KRLogUtil.kr_e('心跳响应连续超时 $_maxHeartbeatTimeout 次,主动断开重连', tag: 'WebSocket');
_handleConnectionError();
}
});
} else {
timer.cancel();
_heartbeatTimeoutTimer?.cancel();
}
});
}
}
/// 处理接收到的消息
void _handleMessage(dynamic message) {
try {
if (message is String) {
if (message == 'ping') {
KRLogUtil.kr_i('收到心跳响应', tag: 'WebSocket');
// 重置心跳超时计数
_heartbeatTimeoutCount = 0;
_heartbeatTimeoutTimer?.cancel();
return;
}
final Map<String, dynamic> data = json.decode(message);
KRLogUtil.kr_i('处理消息: ${json.encode(data)}', tag: 'WebSocket');
_onMessageCallback?.call(data);
}
} catch (e) {
KRLogUtil.kr_e('消息处理错误: $e', tag: 'WebSocket');
}
}
/// 发送消息
void sendMessage(String message) {
try {
if (!_isConnected) {
KRLogUtil.kr_w('WebSocket 未连接,无法发送消息', tag: 'WebSocket');
return;
}
if (_socket == null) {
KRLogUtil.kr_w('WebSocket 实例为空,无法发送消息', tag: 'WebSocket');
return;
}
_socket!.add(message);
KRLogUtil.kr_i('发送消息: $message', tag: 'WebSocket');
} catch (e) {
KRLogUtil.kr_e('发送消息失败: $e', tag: 'WebSocket');
_handleConnectionError();
}
}
/// 发送 JSON 消息
void sendJsonMessage(Map<String, dynamic> message) {
try {
if (!_isConnected) {
KRLogUtil.kr_w('WebSocket 未连接,无法发送消息', tag: 'WebSocket');
return;
}
final jsonString = json.encode(message);
sendMessage(jsonString);
} catch (e) {
KRLogUtil.kr_e('发送 JSON 消息失败: $e', tag: 'WebSocket');
_handleConnectionError();
}
}
/// 清理资源
void _cleanup() {
_heartbeatTimer?.cancel();
_heartbeatTimeoutTimer?.cancel();
_reconnectTimer?.cancel();
_vpnStateChangeTimer?.cancel();
_connectionStabilityTimer?.cancel();
// 取消订阅
_socketSubscription?.cancel();
_socketSubscription = null;
_socket?.close();
_socket = null;
_heartbeatTimer = null;
_heartbeatTimeoutTimer = null;
_reconnectTimer = null;
_vpnStateChangeTimer = null;
_connectionStabilityTimer = null;
_isConnected = false;
_isConnecting = false;
_isConnectionStable = false;
_heartbeatTimeoutCount = 0;
}
/// 关闭连接
Future<void> disconnect() async {
KRLogUtil.kr_i('关闭 WebSocket 连接', tag: 'WebSocket');
_cleanup();
_onConnectionStateCallback?.call(false);
}
/// 检查连接状态
bool get isConnected => _isConnected;
}
+707
View File
@@ -0,0 +1,707 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/model/response/kr_node_group_list.dart';
import 'package:kaer_with_panels/app/model/response/kr_user_available_subscribe.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_subscribe_api.dart';
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import '../../singbox/model/singbox_status.dart';
import '../model/business/kr_group_outbound_list.dart';
import '../model/business/kr_outbound_item.dart';
import '../model/business/kr_outbounds_list.dart';
import '../model/response/kr_already_subscribe.dart';
/// 首页列表视图状态枚举
enum KRSubscribeServiceStatus { kr_none, kr_loading, kr_error, kr_success }
/// 订阅服务类
/// 用于管理用户订阅相关的所有操作
class KRSubscribeService {
/// 单例实例
static final KRSubscribeService _instance = KRSubscribeService._internal();
/// 工厂构造函数
factory KRSubscribeService() => _instance;
/// 私有构造函数
KRSubscribeService._internal() {}
/// 订阅API
final KRSubscribeApi kr_subscribeApi = KRSubscribeApi();
/// 可用订阅列表
final RxList<KRUserAvailableSubscribeItem> kr_availableSubscribes =
<KRUserAvailableSubscribeItem>[].obs;
/// 当前选中的订阅
final Rx<KRUserAvailableSubscribeItem?> kr_currentSubscribe =
Rx<KRUserAvailableSubscribeItem?>(null);
/// 节点分组列表
final RxList<KRNodeGroupListItem> kr_nodeGroups = <KRNodeGroupListItem>[].obs;
/// 服务器分组
final RxList<KRGroupOutboundList> groupOutboundList =
<KRGroupOutboundList>[].obs;
/// 国家分组,包含所有国家
final RxList<KRCountryOutboundList> countryOutboundList =
<KRCountryOutboundList>[].obs;
/// 全部列表
final RxList<KROutboundItem> allList = <KROutboundItem>[].obs;
/// 标签列表
Map<String, KROutboundItem> keyList = {}; // 存储国家分组的列表
/// 试用剩余时间
final RxString kr_trialRemainingTime = ''.obs;
/// 订阅剩余时间
final RxString kr_subscriptionRemainingTime = ''.obs;
/// 剩余时间
final RxString remainingTime = ''.obs;
/// 是否处于试用状态
final RxBool kr_isTrial = false.obs;
/// 当前节点列表是否包含试用节点
final RxBool kr_hasTrialNodes = false.obs;
/// 订阅记录
final RxList<KRAlreadySubscribe> kr_alreadySubscribe =
<KRAlreadySubscribe>[].obs;
/// 是否处于订阅最后一天
final RxBool kr_isLastDayOfSubscription = false.obs;
/// 定期更新计时器
Timer? _kr_updateTimer;
/// 试用倒计时计时器
Timer? _kr_trialTimer;
/// 订阅倒计时计时器
Timer? _kr_subscriptionTimer;
/// 当前状态
final kr_currentStatus = KRSubscribeServiceStatus.kr_none.obs;
/// 重置订阅周期
Future<void> kr_resetSubscribePeriod() async {
if (kr_currentSubscribe.value == null) {
KRCommonUtil.kr_showToast('请先选择订阅');
return;
}
final result = await kr_subscribeApi
.kr_resetSubscribePeriod(kr_currentSubscribe.value!.id);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
KRLogUtil.kr_e('重置订阅周期失败: ${error.msg}', tag: 'SubscribeService');
},
(success) {
kr_refreshAll();
KRLogUtil.kr_i('重置订阅周期成功', tag: 'SubscribeService');
},
);
}
/// 获取可用订阅列表
Future<void> _kr_fetchAvailableSubscribes() async {
try {
KRLogUtil.kr_i('开始获取可用订阅列表', tag: 'SubscribeService');
// 🔧 修复:同时更新已订阅记录列表,确保判断准确
final alreadySubscribeResult = await kr_subscribeApi.kr_getAlreadySubscribe();
alreadySubscribeResult.fold(
(error) {
KRLogUtil.kr_e('获取已订阅列表失败: ${error.msg}', tag: 'SubscribeService');
},
(subscribes) {
kr_alreadySubscribe.value = subscribes;
KRLogUtil.kr_i('更新已订阅记录: ${subscribes.length} 个订阅',
tag: 'SubscribeService');
},
);
final result = await kr_subscribeApi.kr_userAvailableSubscribe();
result.fold(
(error) {
KRLogUtil.kr_e('获取可用订阅失败: ${error.msg}', tag: 'SubscribeService');
},
(subscribes) {
// 如果当前有选中的订阅,检查是否还在可用列表中
if (kr_currentSubscribe.value != null) {
final currentSubscribeExists = subscribes.any(
(subscribe) => subscribe.id == kr_currentSubscribe.value?.id,
);
// 如果当前订阅不在可用列表中,清除当前订阅
if (!currentSubscribeExists) {
// 如果当前订阅为null或者已过期,才设置新的订阅
if (kr_currentSubscribe.value == null ||
DateTime.parse(kr_currentSubscribe.value!.expireTime)
.isBefore(DateTime.now())) {
kr_availableSubscribes.assignAll(subscribes);
if (subscribes.isNotEmpty) {
// 🔧 修复:优先选择已购买的套餐
KRUserAvailableSubscribeItem? selectedSubscribe;
// 优先选择已购买的套餐(非试用)
for (var subscribe in subscribes) {
final isSubscribed = kr_alreadySubscribe.any(
(alreadySub) => alreadySub.userSubscribeId == subscribe.id,
);
if (isSubscribed) {
selectedSubscribe = subscribe;
KRLogUtil.kr_i('选择已购买的套餐: ${selectedSubscribe.name}',
tag: 'SubscribeService');
break;
}
}
// 如果没有已购买的套餐,选择第一个(可能是试用套餐)
if (selectedSubscribe == null) {
selectedSubscribe = subscribes.first;
KRLogUtil.kr_i('没有已购买的套餐,选择第一个: ${selectedSubscribe.name}',
tag: 'SubscribeService');
}
kr_currentSubscribe.value = selectedSubscribe;
} else {
kr_currentSubscribe.value = null;
KRLogUtil.kr_i('没有可用的订阅,清除选中状态', tag: 'SubscribeService');
}
kr_clearCutNodeData();
} else {
KRLogUtil.kr_i('当前订阅仍然有效,保持选中状态', tag: 'SubscribeService');
}
} else {
// 如果当前订阅仍然有效,更新为最新的订阅信息
final updatedSubscribe = subscribes.firstWhere(
(subscribe) => subscribe.id == kr_currentSubscribe.value?.id,
);
// 检查订阅是否有效(未过期且未超出流量限制)
final isExpired = DateTime.parse(updatedSubscribe.expireTime)
.isBefore(DateTime.now());
final isOverTraffic = updatedSubscribe.traffic > 0 &&
(updatedSubscribe.download + updatedSubscribe.upload) >=
updatedSubscribe.traffic;
if (isExpired || isOverTraffic) {
if (KRSingBoxImp.instance.kr_status ==
SingboxStatus.started()) {
KRSingBoxImp.instance.kr_stop();
}
}
kr_currentSubscribe.value = updatedSubscribe;
KRLogUtil.kr_i('更新当前订阅信息', tag: 'SubscribeService');
// 更新可用订阅列表
kr_availableSubscribes.assignAll(subscribes);
}
}
KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
tag: 'SubscribeService');
KRLogUtil.kr_i(
'订阅列表: ${subscribes.map((s) => '${s.name}(${s.id})').join(', ')}',
tag: 'SubscribeService');
},
);
} catch (err) {
KRLogUtil.kr_e('获取可用订阅异常: $err', tag: 'SubscribeService');
}
}
/// 切换订阅
Future<void> kr_switchSubscribe(
KRUserAvailableSubscribeItem subscribe) async {
// 如果切换的是当前订阅,直接返回
if (subscribe.id == kr_currentSubscribe.value?.id) {
KRLogUtil.kr_i('切换的订阅与当前订阅相同,无需切换', tag: 'SubscribeService');
return;
}
try {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_loading;
await kr_clearCutNodeData();
KRLogUtil.kr_i('开始切换订阅: ${subscribe.name + subscribe.id.toString()}',
tag: 'SubscribeService');
// 更新当前订阅
kr_currentSubscribe.value = subscribe;
final result =
await kr_subscribeApi.kr_nodeList(kr_currentSubscribe.value!.id);
result.fold((error) {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
}, (nodes) {
// 记录当前节点列表是否包含试用节点
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('切换订阅 - 节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 处理节点列表(不使用分组)
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, []);
// 更新UI数据
groupOutboundList.value = listModel.groupOutboundList;
countryOutboundList.value = listModel.countryOutboundList;
allList.value = listModel.allList;
keyList = listModel.keyList;
// 保存配置
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
// 更新试用和订阅状态
_kr_updateSubscribeStatus();
kr_currentStatus.value = KRSubscribeServiceStatus.kr_success;
});
} catch (e) {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
KRLogUtil.kr_e('切换订阅失败: $e', tag: 'SubscribeService');
rethrow;
}
}
/// 更新订阅状态
void _kr_updateSubscribeStatus() {
// 停止之前的计时器
_kr_trialTimer?.cancel();
_kr_subscriptionTimer?.cancel();
if (kr_currentSubscribe.value == null) {
kr_isTrial.value = false;
return;
}
// 优先使用 API 返回的 isTryOut 字段判断试用状态
final currentSubscribe = kr_currentSubscribe.value!;
// 1. 优先使用 API 返回的 isTryOut 字段
kr_isTrial.value = currentSubscribe.isTryOut;
KRLogUtil.kr_i('步骤1 - API isTryOut 字段: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
// 2. 如果 API 说不是试用,检查是否有购买记录
if (!kr_isTrial.value) {
final bool kr_isSubscribed = kr_alreadySubscribe.any(
(subscribe) => currentSubscribe.id == subscribe.userSubscribeId
);
KRLogUtil.kr_i('步骤2 - 检查购买记录: $kr_isSubscribed', tag: 'SubscribeService');
// 如果没有购买记录,判断为试用
if (!kr_isSubscribed) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤2 - 没有购买记录,判定为试用', tag: 'SubscribeService');
}
}
// 3. 最后检查订阅名称是否包含"试用"关键字(最后的备用方案)
if (!kr_isTrial.value && currentSubscribe.name.contains('试用')) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤3 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService');
}
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
KRLogUtil.kr_i('当前订阅: ${currentSubscribe.name}(${currentSubscribe.id})', tag: 'SubscribeService');
KRLogUtil.kr_i('API isTryOut: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录数: ${kr_alreadySubscribe.length}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅ID列表: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', tag: 'SubscribeService');
KRLogUtil.kr_i('✅ 最终试用状态: ${kr_isTrial.value ? "试用" : "付费"}', tag: 'SubscribeService');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
if (kr_isTrial.value) {
// 启动试用倒计时
_kr_startTrialTimer();
}
// 检查订阅状态
else if (kr_currentSubscribe.value != null) {
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
final now = DateTime.now();
final difference = expireTime.difference(now);
// 检查是否最后一天
kr_isLastDayOfSubscription.value = difference.inDays <= 1;
if (kr_isLastDayOfSubscription.value) {
// 启动订阅倒计时
_kr_startSubscriptionTimer();
KRLogUtil.kr_i('当前订阅最后一天', tag: 'SubscribeService');
}
}
}
/// 启动试用倒计时
void _kr_startTrialTimer() {
_kr_trialTimer?.cancel();
// 立即执行一次
_kr_updateTrialTime();
// 设置定时器
_kr_trialTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_kr_updateTrialTime();
});
}
/// 更新试用时间
void _kr_updateTrialTime() {
if (kr_currentSubscribe.value == null) {
_kr_trialTimer?.cancel();
return;
}
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
final now = DateTime.now();
final difference = expireTime.difference(now);
if (difference.isNegative) {
/// 停止
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
KRSingBoxImp.instance.kr_stop();
}
_kr_trialTimer?.cancel();
kr_trialRemainingTime.value = 'error.60001'.tr;
return;
}
final days = difference.inDays;
final hours = difference.inHours % 24;
final minutes = difference.inMinutes % 60;
final seconds = difference.inSeconds % 60;
kr_trialRemainingTime.value = AppTranslations.kr_home.trialTimeWithDays(
days,
hours,
minutes,
seconds,
);
}
/// 启动订阅倒计时
void _kr_startSubscriptionTimer() {
_kr_subscriptionTimer?.cancel();
// 立即执行一次
_kr_updateSubscriptionTime();
// 设置定时器
_kr_subscriptionTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_kr_updateSubscriptionTime();
});
}
/// 更新订阅时间
void _kr_updateSubscriptionTime() {
if (kr_currentSubscribe.value == null) {
_kr_subscriptionTimer?.cancel();
return;
}
final expireTime = DateTime.parse(kr_currentSubscribe.value!.expireTime);
final now = DateTime.now();
final difference = expireTime.difference(now);
if (difference.isNegative) {
_kr_subscriptionTimer?.cancel();
/// 停止
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
KRSingBoxImp.instance.kr_stop();
}
kr_subscriptionRemainingTime.value = 'error.60001'.tr;
;
return;
}
final days = difference.inDays;
final hours = difference.inHours % 24;
final minutes = difference.inMinutes % 60;
final seconds = difference.inSeconds % 60;
kr_subscriptionRemainingTime.value =
AppTranslations.kr_home.trialTimeWithDays(
days,
hours,
minutes,
seconds,
);
}
/// 启动定期更新
void _kr_startPeriodicUpdate() {
// 每5分钟更新一次可用订阅列表
_kr_updateTimer = Timer.periodic(const Duration(seconds: 60), (timer) {
_kr_fetchAvailableSubscribes();
});
}
/// 刷新所有数据
Future<void> kr_refreshAll() async {
try {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_loading;
await kr_clearData();
KRLogUtil.kr_i('开始刷新所有数据', tag: 'SubscribeService');
/// 数组有值 ,表示订阅过, 用于判断试用的
final alreadySubscribeResult =
await kr_subscribeApi.kr_getAlreadySubscribe();
alreadySubscribeResult.fold(
(error) {
throw Exception('获取已订阅列表失败: ${error.msg}');
},
(subscribes) {
kr_alreadySubscribe.value = subscribes;
KRLogUtil.kr_i('订阅记录: ${subscribes.length} 个订阅',
tag: 'SubscribeService');
},
);
// 🔧 取消节点分组的概念,不再调用 kr_nodeGroupList
// 直接使用节点列表,通过 is_try_out 字段区分免费/付费节点
kr_nodeGroups.clear();
KRLogUtil.kr_i('已取消节点分组,将直接使用节点列表', tag: 'SubscribeService');
// 保存当前选中的订阅名称
final currentSubscribeID = kr_currentSubscribe.value?.id;
// 获取可用订阅列表
final subscribeResult = await kr_subscribeApi.kr_userAvailableSubscribe();
// 处理订阅列表结果
final subscribes = await subscribeResult.fold(
(error) {
throw Exception('获取可用订阅失败: ${error.msg}');
},
(subscribes) => subscribes,
);
// 如果获取订阅列表为空(试用结束且未购买),设置为成功状态但清空订阅
if (subscribes.isEmpty) {
KRLogUtil.kr_i('订阅列表为空(试用结束或未购买),清空订阅数据', tag: 'SubscribeService');
kr_availableSubscribes.clear();
kr_currentSubscribe.value = null;
kr_currentStatus.value = KRSubscribeServiceStatus.kr_success;
return;
}
// 更新订阅列表
kr_availableSubscribes.assignAll(subscribes);
KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
tag: 'SubscribeService');
// 如果之前的订阅名称在可用列表中,保持选中
if (subscribes.isNotEmpty) {
KRUserAvailableSubscribeItem? selectedSubscribe;
// 1. 首先尝试找到之前选中的订阅
if (currentSubscribeID != null) {
try {
selectedSubscribe = subscribes.firstWhere(
(subscribe) => subscribe.id == currentSubscribeID,
);
KRLogUtil.kr_i('保持之前选中的订阅: ${selectedSubscribe.name}',
tag: 'SubscribeService');
} catch (e) {
// 没找到之前的订阅,继续下一步
KRLogUtil.kr_i('之前选中的订阅已不存在,重新选择', tag: 'SubscribeService');
}
}
// 2. 如果没有找到之前的订阅,优先选择试用套餐
if (selectedSubscribe == null) {
KRLogUtil.kr_i('开始查找试用套餐...', tag: 'SubscribeService');
for (var subscribe in subscribes) {
KRLogUtil.kr_i('检查订阅: ${subscribe.name}(${subscribe.id}), 是否试用: ${subscribe.isTryOut}',
tag: 'SubscribeService');
if (subscribe.isTryOut) {
selectedSubscribe = subscribe;
KRLogUtil.kr_i('✅ 找到试用套餐,默认选择: ${selectedSubscribe.name}',
tag: 'SubscribeService');
break;
}
}
}
// 3. 如果没有试用套餐,选择已购买的套餐(非试用)
if (selectedSubscribe == null) {
KRLogUtil.kr_i('没有试用套餐,查找已购买的套餐...', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
tag: 'SubscribeService');
for (var subscribe in subscribes) {
final isSubscribed = kr_alreadySubscribe.any(
(alreadySub) => alreadySub.userSubscribeId == subscribe.id,
);
KRLogUtil.kr_i('检查订阅: ${subscribe.name}(${subscribe.id}), 是否已购买: $isSubscribed',
tag: 'SubscribeService');
if (isSubscribed) {
selectedSubscribe = subscribe;
KRLogUtil.kr_i('选择已购买的套餐: ${selectedSubscribe.name}',
tag: 'SubscribeService');
break;
}
}
}
// 4. 如果都没有找到,选择第一个
if (selectedSubscribe == null) {
selectedSubscribe = subscribes.first;
KRLogUtil.kr_i('没有找到匹配的套餐,选择第一个: ${selectedSubscribe.name}',
tag: 'SubscribeService');
}
kr_currentSubscribe.value = selectedSubscribe;
// 获取节点列表
final nodeResult =
await kr_subscribeApi.kr_nodeList(kr_currentSubscribe.value!.id);
// 处理节点列表结果
final nodes = await nodeResult.fold(
(error) {
throw Exception('获取节点列表失败: ${error.msg}');
},
(nodes) => nodes,
);
// 记录当前节点列表是否包含试用节点
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 处理节点列表(不使用分组)
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, []);
// 更新UI数据
groupOutboundList.value = listModel.groupOutboundList;
countryOutboundList.value = listModel.countryOutboundList;
allList.value = listModel.allList;
keyList = listModel.keyList;
// 保存配置
KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList);
// 更新试用和订阅状态
_kr_updateSubscribeStatus();
kr_currentStatus.value = KRSubscribeServiceStatus.kr_success;
_kr_startPeriodicUpdate();
} else {
KRLogUtil.kr_w('没有可用的订阅', tag: 'SubscribeService');
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
return;
}
} catch (err, stackTrace) {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
KRLogUtil.kr_e('刷新数据异常: $err\n$stackTrace', tag: 'SubscribeService');
}
}
//// 清楚
Future<void> kr_clearData() async {
_kr_subscriptionTimer?.cancel();
_kr_trialTimer?.cancel();
_kr_updateTimer?.cancel();
kr_availableSubscribes.clear();
await kr_clearCutNodeData();
}
Future<void> kr_logout() async {
kr_alreadySubscribe.clear();
kr_nodeGroups.clear();
kr_currentSubscribe.value = null;
await kr_clearData();
}
Future<void> kr_clearCutNodeData() async {
kr_isLastDayOfSubscription.value = false;
kr_isTrial.value = false;
kr_hasTrialNodes.value = false;
kr_subscriptionRemainingTime.value = '';
kr_trialRemainingTime.value = '';
/// 停止
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
await KRSingBoxImp.instance.kr_stop();
}
// 更新UI数据
groupOutboundList.clear();
countryOutboundList.clear();
allList.clear();
keyList.clear();
// 保存配置
KRSingBoxImp.instance.kr_saveOutbounds([]);
}
/// 获取当前订阅
KRUserAvailableSubscribeItem? get kr_getCurrentSubscribe =>
kr_currentSubscribe.value;
/// 获取免费节点列表(试用节点)
/// 当试用结束时,返回空列表
List<KROutboundItem> get kr_freeNodes {
if (!kr_hasTrialNodes.value) {
return [];
}
// 如果当前是试用状态或有试用节点,返回所有节点作为免费节点
// 如果试用已结束(kr_isTrial=false 且 kr_hasTrialNodes=true),返回空列表
if (kr_isTrial.value) {
KRLogUtil.kr_i('返回免费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
} else {
KRLogUtil.kr_i('试用已结束,不显示免费节点', tag: 'SubscribeService');
return [];
}
}
/// 获取付费节点列表
/// 如果当前不是试用,返回所有节点
/// 如果当前是试用,返回空列表
List<KROutboundItem> get kr_paidNodes {
if (kr_isTrial.value) {
KRLogUtil.kr_i('当前是试用状态,不显示付费节点', tag: 'SubscribeService');
return [];
} else {
KRLogUtil.kr_i('返回付费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
}
}
/// 是否应该显示免费标签页
bool get kr_shouldShowFreeTab {
return kr_hasTrialNodes.value && kr_isTrial.value;
}
/// 是否应该显示付费标签页
bool get kr_shouldShowPaidTab {
return !kr_isTrial.value;
}
}
+663
View File
@@ -0,0 +1,663 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service.dart';
import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import '../../../core/model/directories.dart';
import '../../../singbox/model/singbox_config_option.dart';
import '../../../singbox/model/singbox_outbound.dart';
import '../../../singbox/model/singbox_stats.dart';
import '../../../singbox/model/singbox_status.dart';
import '../../utils/kr_country_util.dart';
import '../../utils/kr_log_util.dart';
enum KRConnectionType {
global,
rule,
// direct,
}
class KRSingBoxImp {
/// 私有构造函数
KRSingBoxImp._();
/// 单例实例
static final KRSingBoxImp _instance = KRSingBoxImp._();
/// 工厂构造函数
factory KRSingBoxImp() => _instance;
/// 获取实例的静态方法
static KRSingBoxImp get instance => _instance;
/// 配置文件目录
late Directories kr_configDics;
/// 配置文件名称
String kr_configName = "BearVPN";
/// 通道方法
final _kr_methodChannel = const MethodChannel("com.baer.app/platform");
final _kr_container = ProviderContainer();
/// 核心服务
late SingboxService kr_singBox;
/// more配置
Map<String, dynamic> kr_configOption = {};
List<Map<String, dynamic>> kr_outbounds = [];
/// 首次启动
RxBool kr_isFristStart = false.obs;
/// 状态
final kr_status = SingboxStatus.stopped().obs;
/// 拦截广告
final kr_blockAds = true.obs;
/// 是否自动自动选择线路
final kr_isAutoOutbound = true.obs;
/// 连接类型
final kr_connectionType = KRConnectionType.rule.obs;
String _cutPath = "";
/// 端口
int kr_port = 51213;
/// 统计
final kr_stats = SingboxStats(
connectionsIn: 0,
connectionsOut: 0,
uplink: 0,
downlink: 0,
uplinkTotal: 0,
downlinkTotal: 0,
).obs;
/// 活动的出站分组
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
/// 所有的出站分组
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
/// Stream 订阅管理器
final List<StreamSubscription<dynamic>> _kr_subscriptions = [];
/// 初始化
Future<void> init() async {
try {
KRLogUtil.kr_i('开始初始化 SingBox');
// 在应用启动时初始化
await KRCountryUtil.kr_init();
KRLogUtil.kr_i('国家工具初始化完成');
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
KRLogUtil.kr_i('配置选项初始化完成');
KRLogUtil.kr_i('开始初始化 SingBox 服务');
kr_singBox = await _kr_container.read(singboxServiceProvider);
await _kr_container.read(singboxServiceProvider).init();
KRLogUtil.kr_i('SingBox 服务初始化完成');
KRLogUtil.kr_i('开始初始化目录');
/// 初始化目录
if (Platform.isIOS) {
final paths = await _kr_methodChannel.invokeMethod<Map>("get_paths");
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
kr_configDics = (
baseDir: Directory(paths?["base"]! as String),
workingDir: Directory(paths?["working"]! as String),
tempDir: Directory(paths?["temp"]! as String),
);
} else {
final baseDir = await getApplicationSupportDirectory();
final workingDir =
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
final tempDir = await getTemporaryDirectory();
kr_configDics = (
baseDir: baseDir,
workingDir: workingDir!,
tempDir: tempDir,
);
KRLogUtil.kr_i('其他平台路径初始化完成');
}
KRLogUtil.kr_i('开始创建目录');
if (!kr_configDics.baseDir.existsSync()) {
await kr_configDics.baseDir.create(recursive: true);
}
if (!kr_configDics.workingDir.existsSync()) {
await kr_configDics.workingDir.create(recursive: true);
}
if (!kr_configDics.tempDir.existsSync()) {
await kr_configDics.tempDir.create(recursive: true);
}
if (!directory.existsSync()) {
await directory.create(recursive: true);
}
KRLogUtil.kr_i('目录创建完成');
KRLogUtil.kr_i('开始设置 SingBox');
await kr_singBox.setup(kr_configDics, false).map((r) {
KRLogUtil.kr_i('SingBox 设置成功');
}).mapLeft((err) {
KRLogUtil.kr_e('SingBox 设置失败: $err');
throw err;
}).run();
KRLogUtil.kr_i('开始更新 SingBox 选项');
KRLogUtil.kr_i('📋 SingBox 配置选项: ${oOption.toJson()}', tag: 'SingBox');
await kr_singBox.changeOptions(oOption)
..map((r) {
KRLogUtil.kr_i('✅ SingBox 选项更新成功', tag: 'SingBox');
}).mapLeft((err) {
KRLogUtil.kr_e('❌ SingBox 选项更新失败: $err', tag: 'SingBox');
throw err;
}).run();
KRLogUtil.kr_i('开始监听状态');
// 初始订阅状态流
kr_singBox.watchStatus().listen((status) {
KRLogUtil.kr_i('🔄 SingBox 状态变化: $status', tag: 'SingBox');
KRLogUtil.kr_i('📊 状态类型: ${status.runtimeType}', tag: 'SingBox');
// 确保状态更新
kr_status.value = status;
switch (status) {
case SingboxStopped():
KRLogUtil.kr_i('🔴 SingBox 已停止', tag: 'SingBox');
break;
case SingboxStarting():
KRLogUtil.kr_i('🟡 SingBox 正在启动', tag: 'SingBox');
break;
case SingboxStarted():
KRLogUtil.kr_i('🟢 SingBox 已启动', tag: 'SingBox');
kr_isFristStart.value = true;
// 使用 GetX 的方式处理 Stream 订阅
_kr_subscribeToStats();
_kr_subscribeToGroups();
// 强制触发状态更新
kr_status.refresh();
// 🔧 修复:启动后检查活动组是否正常更新(超时保护)
Future.delayed(const Duration(seconds: 3), () {
if (kr_activeGroups.isEmpty && kr_status.value == SingboxStarted()) {
KRLogUtil.kr_w('⚠️ SingBox已启动3秒但活动组仍为空,尝试重新订阅', tag: 'SingBox');
_kr_subscribeToGroups();
}
});
break;
case SingboxStopping():
KRLogUtil.kr_i('🟠 SingBox 正在停止', tag: 'SingBox');
break;
}
});
KRLogUtil.kr_i('SingBox 初始化完成');
} catch (e, stackTrace) {
KRLogUtil.kr_e('SingBox 初始化失败: $e');
KRLogUtil.kr_e('错误堆栈: $stackTrace');
rethrow;
}
}
Map<String, dynamic> _getConfigOption() {
if (kr_configOption.isNotEmpty) {
return kr_configOption;
}
final op = {
"region": KRCountryUtil.kr_getCurrentCountryCode(),
"block-ads": kr_blockAds.value,
"use-xray-core-when-possible": false,
"execute-config-as-is": false,
"log-level": "warn",
"resolve-destination": false,
"ipv6-mode": "ipv4_only",
// "remote-dns-address": "https://cloudflare-dns.com/dns-query",
"remote-dns-address": "udp://1.1.1.1",
"remote-dns-domain-strategy": "",
"direct-dns-address": "223.5.5.5",
"direct-dns-domain-strategy": "",
"mixed-port": kr_port,
"tproxy-port": kr_port,
"local-dns-port": 36450,
"tun-implementation": "gvisor",
"mtu": 9000,
"strict-route": true,
// "connection-test-url": "http://www.cloudflare.com",
"connection-test-url": "http://www.gstatic.com/generate_204",
"url-test-interval": 30,
"enable-clash-api": true,
"clash-api-port": 36756,
"enable-tun": Platform.isIOS || Platform.isAndroid,
"enable-tun-service": false,
"set-system-proxy":
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
"bypass-lan": false,
"allow-connection-from-lan": false,
"enable-fake-dns": false,
"enable-dns-routing": true,
"independent-dns-cache": true,
"rules": [],
"mux": {
"enable": false,
"padding": false,
"max-streams": 8,
"protocol": "h2mux"
},
"tls-tricks": {
"enable-fragment": false,
"fragment-size": "10-30",
"fragment-sleep": "2-8",
"mixed-sni-case": false,
"enable-padding": false,
"padding-size": "1-1500"
},
"warp": {
"enable": false,
"mode": "proxy_over_warp",
"wireguard-config": "",
"license-key": "",
"account-id": "",
"access-token": "",
"clean-ip": "auto",
"clean-port": 0,
"noise": "1-3",
"noise-size": "10-30",
"noise-delay": "10-30",
"noise-mode": "m4"
},
"warp2": {
"enable": false,
"mode": "proxy_over_warp",
"wireguard-config": "",
"license-key": "",
"account-id": "",
"access-token": "",
"clean-ip": "auto",
"clean-port": 0,
"noise": "1-3",
"noise-size": "10-30",
"noise-delay": "10-30",
"noise-mode": "m4"
}
};
kr_configOption = op;
return op;
}
/// 订阅统计数据流
void _kr_subscribeToStats() {
// 取消之前的统计订阅
for (var sub in _kr_subscriptions) {
if (sub.hashCode.toString().contains('Stats')) {
sub.cancel();
}
}
_kr_subscriptions
.removeWhere((sub) => sub.hashCode.toString().contains('Stats'));
_kr_subscriptions.add(
kr_singBox.watchStats().listen(
(stats) {
kr_stats.value = stats;
},
onError: (error) {
KRLogUtil.kr_e('统计数据监听错误: $error');
},
cancelOnError: false,
),
);
}
/// 订阅分组数据流
void _kr_subscribeToGroups() {
// 取消之前的分组订阅
for (var sub in _kr_subscriptions) {
if (sub.hashCode.toString().contains('Groups')) {
sub.cancel();
}
}
_kr_subscriptions
.removeWhere((sub) => sub.hashCode.toString().contains('Groups'));
_kr_subscriptions.add(
kr_singBox.watchActiveGroups().listen(
(groups) {
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
kr_activeGroups.value = groups;
// 详细打印每个组的信息
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
},
onError: (error) {
KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox');
},
cancelOnError: false,
),
);
_kr_subscriptions.add(
kr_singBox.watchGroups().listen(
(groups) {
kr_allGroups.value = groups;
},
onError: (error) {
KRLogUtil.kr_e('所有分组监听错误: $error');
},
cancelOnError: false,
),
);
}
/// 监听活动组的详细实现
// Future<void> watchActiveGroups() async {
// try {
// print("开始监听活动组详情...");
// final status = await kr_singBox.status();
// print("服务状态: ${status.toJson()}");
// final outbounds = await kr_singBox.listOutbounds();
// print("出站列表: ${outbounds.toJson()}");
// for (var outbound in outbounds.outbounds) {
// print("出站配置: ${outbound.toJson()}");
// // 检查出站是否活动
// final isActive = await kr_singBox.isOutboundActive(outbound.tag);
// print("出站 ${outbound.tag} 活动状态: $isActive");
// }
// } catch (e, stack) {
// print("监听活动组详情时出错: $e");
// print("错误堆栈: $stack");
// }
// }
/// 保存配置文件
void kr_saveOutbounds(List<Map<String, dynamic>> outbounds) async {
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
kr_outbounds = outbounds;
final map = {};
map["outbounds"] = kr_outbounds;
final file = _file(kr_configName);
final temp = _tempFile(kr_configName);
final mapStr = jsonEncode(map);
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置文件前500字符: ${mapStr.substring(0, mapStr.length > 500 ? 500 : mapStr.length)}', tag: 'SingBox');
await file.writeAsString(mapStr);
await temp.writeAsString(mapStr);
_cutPath = file.path;
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
await kr_singBox
.validateConfigByPath(file.path, temp.path, false)
.mapLeft((err) {
KRLogUtil.kr_e('❌ 保存配置文件失败: $err', tag: 'SingBox');
}).run();
KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox');
}
Future<void> kr_start() async {
kr_status.value = SingboxStarting();
try {
KRLogUtil.kr_i('🚀 开始启动 SingBox...', tag: 'SingBox');
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox');
// 检查配置文件是否存在
final configFile = File(_cutPath);
if (await configFile.exists()) {
final configContent = await configFile.readAsString();
KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox');
} else {
KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox');
}
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
},
).mapLeft((err) {
KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox');
// 确保状态重置为Stopped,触发UI更新
kr_status.value = SingboxStopped();
// 强制刷新状态以触发观察者
kr_status.refresh();
throw err;
}).run();
} catch (e, stackTrace) {
KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
// 确保状态重置为Stopped,触发UI更新
kr_status.value = SingboxStopped();
// 强制刷新状态以触发观察者
kr_status.refresh();
rethrow;
}
}
/// 停止服务
Future<void> kr_stop() async {
try {
// 不主动赋值 kr_status
await Future.delayed(const Duration(milliseconds: 100));
await kr_singBox.stop().run();
await Future.delayed(const Duration(milliseconds: 1000));
// 取消订阅
final subscriptions = List<StreamSubscription<dynamic>>.from(_kr_subscriptions);
_kr_subscriptions.clear();
for (var subscription in subscriptions) {
try {
await subscription.cancel();
} catch (e) {
KRLogUtil.kr_e('取消订阅时出错: $e');
}
}
// 不主动赋值 kr_status
} catch (e, stackTrace) {
KRLogUtil.kr_e('停止服务时出错: $e');
KRLogUtil.kr_e('错误堆栈: $stackTrace');
// 兜底,防止状态卡死
kr_status.value = SingboxStopped();
rethrow;
}
}
///
void kr_updateAdBlockEnabled(bool bl) async {
final oOption = _getConfigOption();
oOption["block-ads"] = bl;
final op = SingboxConfigOption.fromJson(oOption);
await kr_singBox.changeOptions(op)
..map((r) {}).mapLeft((err) {
KRLogUtil.kr_e('更新广告拦截失败: $err');
}).run();
if (kr_status.value == SingboxStarted()) {
await kr_restart();
}
kr_blockAds.value = bl;
}
Future<void> kr_restart() async {
KRLogUtil.kr_i("restart");
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
KRLogUtil.kr_e('重启失败: $err');
}).run();
}
//// 设置出站模式
Future<void> kr_updateConnectionType(KRConnectionType newType) async {
if (kr_connectionType.value == newType) {
return;
}
kr_connectionType.value = newType;
final oOption = _getConfigOption();
var mode = "";
switch (newType) {
case KRConnectionType.global:
mode = "other";
break;
case KRConnectionType.rule:
mode = KRCountryUtil.kr_getCurrentCountryCode();
break;
// case KRConnectionType.direct:
// mode = "direct";
// break;
}
oOption["region"] = mode;
final op = SingboxConfigOption.fromJson(oOption);
await kr_singBox.changeOptions(op)
..map((r) {}).mapLeft((err) {
KRLogUtil.kr_e('更新连接类型失败: $err');
}).run();
if (kr_status.value == SingboxStarted()) {
await kr_restart();
}
}
/// 更新国家设置
Future<void> kr_updateCountry(KRCountry kr_country) async {
// 如果国家相同,直接返回
if (kr_country.kr_code == KRCountryUtil.kr_getCurrentCountryCode()) {
return;
}
try {
// 更新工具类中的当前国家
await KRCountryUtil.kr_setCurrentCountry(kr_country);
// 更新配置选项
final oOption = _getConfigOption();
oOption["region"] = kr_country.kr_code;
final op = SingboxConfigOption.fromJson(oOption);
await kr_singBox.changeOptions(op)
..map((r) {}).mapLeft((err) {
KRLogUtil.kr_e('更新国家设置失败: $err');
}).run();
// 如果服务正在运行,重启服务
if (kr_status.value == SingboxStarted()) {
await kr_restart();
}
} catch (err) {
KRLogUtil.kr_e('更新国家失败: $err');
rethrow;
}
}
Stream<SingboxStatus> kr_watchStatus() {
return kr_singBox.watchStatus();
}
Stream<List<SingboxOutboundGroup>> kr_watchGroups() {
return kr_singBox.watchGroups();
}
void kr_selectOutbound(String tag) {
KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', tag: 'SingBox');
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
// 打印所有活动组信息
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
kr_singBox.selectOutbound("select", tag).run();
}
/// 配合文件地址
Directory get directory =>
Directory(p.join(kr_configDics.workingDir.path, "configs"));
File _file(String fileName) {
return File(p.join(directory.path, "$fileName.json"));
}
File _tempFile(String fileName) => _file("$fileName.tmp");
// File tempFile(String fileName) => file("$fileName.tmp");
Future<void> kr_urlTest(String groupTag) async {
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
// 打印所有活动组信息
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
try {
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
final result = await kr_singBox.urlTest(groupTag).run();
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
// 等待一段时间让 SingBox 完成测试
await Future.delayed(const Duration(seconds: 2));
// 再次检查活动组状态
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
}
}
} catch (e) {
KRLogUtil.kr_e('❌ URL 测试失败: $groupTag, 错误: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
}
}
}