Browse Source

refactor(version_update): 重构版本更新服务以支持多渠道配置

- 将单渠道查询改为获取所有渠道信息并建立索引
- 新增渠道信息解析和索引构建方法
- 实现主备渠道选择策略
- 移除冗余的_refreshTouristsFromEnv方法
- 优化游客登录和跳过设备绑定配置逻辑
weicu
lxm 10 months ago
parent
commit
e6d91b4f1e
  1. 33
      lib/data/services/network/api.dart
  2. 132
      lib/data/services/version_update_service.dart

33
lib/data/services/network/api.dart

@ -244,36 +244,37 @@ class Api {
); );
} }
/// 资源使用上报(`/api/home/user_usages`) /// 资源使用/用量上报接口(`/api/home/user_usages`)
/// ///
/// 用途:在 AI 聊天等场景中,上报本次会话的消耗与功能调用次数,服务端据此统计并更新积分。 /// 用途:上报翻译(Translation)和会议(Meet)的用量,服务端据此统计并回传最新余额/积分。
/// ///
/// 请求方式:`POST /api/home/user_usages` /// 当前在 Dart 侧的使用场景:
/// /// - 会议助手 ASR:`UsageStatsService._reportMeetingUsageToServer`
/// 认证:已通过全局拦截器自动携带 `Authorization`(见 `AuthInterceptor`)。 /// - 翻译/同传 ASR:`UsageStatsService._reportAsrUsageToServer`
/// ///
/// 参数(`params`)结构: /// 参数(`params`)结构:
/// - `usagetype`:`int` 类型,使用场景类型;Android 端 Agent 使用 `3` 表示 AICHAT。 /// - `usagetype`:`int` 类型,使用场景类型(对应 `UsageType.toInt`),目前仅使用:
/// - `1`:翻译(Translation)
/// - `2`:会议(Meet)
/// - `usages`:`Map<String, int>`,各项指标的键值对,例如: /// - `usages`:`Map<String, int>`,各项指标的键值对,例如:
/// - `AICHAT_PROMPT_TOKENS`:本次对话的提示词 Token 数 /// - `MEETING_ASR_AUDIO_SECONDS`:会议识别音频时长(秒)
/// - `AICHAT_COMPLETION_TOKENS`:本次对话的生成 Token 数 /// - `TRANSLATION_MODE_{mode}`:某翻译模式下累计有效识别秒数
/// - `AICHAT_FUNCTION_{函数名}`:被调用的函数次数(按函数名累加) /// - `TRANSLATION_ASR_LANGUAGE_{language}`:某语言/语言对下的调用次数
/// ///
/// 返回:标准封装后 `code == 0` 时返回 `data`。其中 `data.usages` 可能包含键 `"3"`,表示最新的 AI 积分值。 /// 返回:
/// - 经过 `AuthInterceptor` 处理后,本方法返回值即为服务端响应体中的 `data` 字段。
/// - 若上报成功,`data['usages']` 为 `Map<String, int>`,内部各键的含义与积分/余额更新规则由服务端约定。
/// ///
/// 使用示例: /// 使用示例(Dart):
/// ```dart /// ```dart
/// final params = { /// final params = {
/// 'usagetype': 3, // AICHAT /// 'usagetype': UsageType.Meet.toInt,
/// 'usages': { /// 'usages': {
/// 'AICHAT_PROMPT_TOKENS': 120, /// 'MEETING_ASR_AUDIO_SECONDS': 120,
/// 'AICHAT_COMPLETION_TOKENS': 256,
/// 'AICHAT_FUNCTION_toolX': 1,
/// } /// }
/// }; /// };
/// final data = await Api.usagesResPoints(params); /// final data = await Api.usagesResPoints(params);
/// final usages = (data['usages'] as Map?)?.cast<String, dynamic>(); /// final usages = (data['usages'] as Map?)?.cast<String, dynamic>();
/// final aiIntegral = usages?['3']; // 服务端返回的最新 AI 积分(如有)
/// ``` /// ```
static usagesResPoints([params]) { static usagesResPoints([params]) {
return DioManager().request( return DioManager().request(

132
lib/data/services/version_update_service.dart

@ -124,16 +124,15 @@ class VersionUpdateService extends GetxService {
//_channel = platformInfo.channel; //_channel = platformInfo.channel;
_lastCheckTime = DateTime.now(); _lastCheckTime = DateTime.now();
// 调用后端接口获取最新版本信息 // 获取所有渠道包信息
final response = final response = await Api.getChannelApps();
await Api.getChannelApp({'channel': platformInfo.channel.value});
// final response = await Api.getChannelApps();
_logger.d('lxm--获取渠道包信息:$response'); _logger.d('lxm--获取所有渠道包信息:$response');
if (response == null || !response.containsKey('app')) { final List<AppInfo> apps = _parseAppsList(response);
//[2]获取渠道包信息失败: 响应为空或不包含app字段
_logger.w('lxm--获取渠道包信息失败: 响应为空或不包含app字段'); if (apps.isEmpty) {
_logger.w('lxm--获取所有渠道包信息失败或为空');
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: false, hasUpdate: false,
currentVersion: currentVersion, currentVersion: currentVersion,
@ -142,24 +141,38 @@ class VersionUpdateService extends GetxService {
); );
} }
//final channelAppResponse = ChannelAppResponse.fromJson(response); final Map<UserChannel, AppInfo> appIndex = _buildChannelIndex(apps);
// 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']);
_appInfo = appInfo;
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
_latestVersion = appInfo.version;
_updateUrl = appInfo.address;
// 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致) // 依据设备品牌渠道用于版本更新链接
_isShowTourists = appInfo.tourists; final brandChannel = platformInfo.channel;
_allowSkipDeviceBinding = appInfo.allowskipdevicebinding;
//print('lxm--_isShowTourists: $_isShowTourists'); // 环境变量渠道用于游客登录配置
if (Platform.isAndroid && _channel != platformInfo.channel) { final envChannelName = dotenv.env['Channel'] ?? 'OFFICIAL';
//[4]当前渠道与.env渠道不同,需要从.env渠道获取游客登录配置 final envChannel = UserChannel.fromName(envChannelName);
await _refreshTouristsFromEnv(); final AppInfo? envAppInfo = appIndex[envChannel];
// 若不存在品牌渠道信息,降级为环境渠道;仍不存在则返回无更新
final AppInfo? chosenAppInfo =
_getChannelAppInfo(appIndex, brandChannel, envChannel);
if (chosenAppInfo == null) {
_logger.w('lxm--未找到品牌或环境渠道的应用信息,返回无更新');
return VersionUpdateInfo(
hasUpdate: false,
currentVersion: currentVersion,
latestVersion: currentVersion,
updateType: UpdateType.none,
);
} }
_appInfo = chosenAppInfo;
_latestVersion = chosenAppInfo.version;
_updateUrl = chosenAppInfo.address;
// 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致)
_isShowTourists = (envAppInfo ?? chosenAppInfo).tourists;
_allowSkipDeviceBinding =
(envAppInfo ?? chosenAppInfo).allowskipdevicebinding;
_logger.d('lxm--从服务器获取游客登录配置(最终): ${_isShowTourists}'); _logger.d('lxm--从服务器获取游客登录配置(最终): ${_isShowTourists}');
// if (channelAppResponse.code != 0 || channelAppResponse.data == null) { // if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
@ -173,20 +186,21 @@ class VersionUpdateService extends GetxService {
// } // }
// final appInfo = channelAppResponse.data!.app; // final appInfo = channelAppResponse.data!.app;
_hasUpdate = _compareVersions(currentVersion, appInfo.version); _hasUpdate = _compareVersions(currentVersion, chosenAppInfo.version);
// 判断更新类型:如果主版本号不同则强制更新 // 判断更新类型:如果主版本号不同则强制更新
UpdateType updateType = UpdateType.none; UpdateType updateType = UpdateType.none;
if (_hasUpdate!) { if (_hasUpdate!) {
_isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version); _isForceUpdate =
_shouldForceUpdate(currentVersion, chosenAppInfo.version);
updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional; updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
} }
//[3]返回版本更新信息 //[3]返回版本更新信息
return VersionUpdateInfo( return VersionUpdateInfo(
hasUpdate: _hasUpdate!, hasUpdate: _hasUpdate!,
currentVersion: currentVersion.split('+')[0], currentVersion: currentVersion.split('+')[0],
latestVersion: appInfo.version.split('+')[0], latestVersion: chosenAppInfo.version.split('+')[0],
appInfo: appInfo, appInfo: chosenAppInfo,
updateType: updateType, updateType: updateType,
); );
} catch (e) { } catch (e) {
@ -296,33 +310,6 @@ class VersionUpdateService extends GetxService {
} }
} }
/// 从.env文件刷新游客登录配置
Future<void> _refreshTouristsFromEnv() async {
try {
final envChannelName = dotenv.env['Channel'];
if (envChannelName == null || envChannelName.isEmpty) {
_logger.w('未配置.env Channel,沿用当前渠道的游客配置: ${_isShowTourists}');
return;
}
final envChannel = UserChannel.fromName(envChannelName);
final resp = await Api.getChannelApp({'channel': envChannel.value});
// _logger.d('lxm--获取渠道包信息:$resp');
if (resp != null && resp.containsKey('app')) {
final envAppInfo = AppInfo.fromJson(resp['app']);
// print('lxm--envAppInfo: ${envAppInfo.toJson()}');
_isShowTourists = envAppInfo.tourists;
_allowSkipDeviceBinding = envAppInfo.allowskipdevicebinding;
_logger.d(
'lxm--游客登录配置已按.env${envChannel.value}渠道同步: ${envAppInfo.tourists} (channel=$envChannelName),_isShowTourists: $_isShowTourists,_allowSkipDeviceBinding: $_allowSkipDeviceBinding');
} else {
_logger.w('按.env渠道获取游客配置失败:响应为空或不含app字段');
}
} catch (e) {
_logger.w('按.env渠道同步游客配置失败: $e');
}
}
/// 获取iOS设备品牌信息 /// 获取iOS设备品牌信息
Future<void> _getIosDeviceBrand() async { Future<void> _getIosDeviceBrand() async {
try { try {
@ -415,6 +402,45 @@ class VersionUpdateService extends GetxService {
return latestVersions.length > currentVersions.length; return latestVersions.length > currentVersions.length;
} }
/// 解析响应数据为AppInfo列表
List<AppInfo> _parseAppsList(dynamic response) {
try {
final appsJson = (response is Map) ? response['apps'] : null;
if (appsJson is List) {
return appsJson
.whereType<Map<String, dynamic>>()
.map((e) => AppInfo.fromJson(e))
.toList();
}
} catch (e) {
_logger.e('解析渠道列表失败: $e');
}
return [];
}
/// 构建渠道索引
Map<UserChannel, AppInfo> _buildChannelIndex(List<AppInfo> apps) {
final index = <UserChannel, AppInfo>{};
for (final app in apps) {
index[app.channel] = app;
}
return index;
}
/// 获取渠道信息,带备选策略
///
/// 优先返回主渠道的信息,如果主渠道不存在则返回备选渠道的信息。
/// 主要用于:
/// 1. 版本更新:优先使用设备品牌渠道,备选使用环境变量渠道
/// 2. 功能配置:优先使用环境变量渠道,备选使用版本更新渠道
AppInfo? _getChannelAppInfo(
Map<UserChannel, AppInfo> index,
UserChannel primary,
UserChannel? fallback,
) {
return index[primary] ?? (fallback != null ? index[fallback] : null);
}
/// 解析版本号字符串为数字数组 /// 解析版本号字符串为数字数组
List<int> _parseVersion(String version) { List<int> _parseVersion(String version) {
final parts = version.split('.'); final parts = version.split('.');

Loading…
Cancel
Save