网络请求打通,ws 打通
This commit is contained in:
@@ -11,21 +11,23 @@ class ApiPaths {
|
||||
ApiPaths._();
|
||||
|
||||
// ── Auth ──
|
||||
static const authLogin = '/auth/login';
|
||||
static const authRefreshToken = '/auth/refresh-token';
|
||||
static const authLogout = '/auth/logout';
|
||||
static const authSendOtp = '/app/api/auth/vcode/get';
|
||||
static const authVerifyOtp = '/app/api/auth/vcode/check';
|
||||
static const authLogin = '/app/api/auth/login-user';
|
||||
static const authRefreshToken = '/app/api/auth/refresh-token';
|
||||
static const authLogout = '/app/api/auth/logout';
|
||||
|
||||
// ── User ──
|
||||
static const userProfile = '/user/profile';
|
||||
static const userUpdateProfile = '/user/update-profile';
|
||||
static const userProfile = '/app/api/user/profile';
|
||||
static const userUpdateProfile = '/app/api/user/update-profile';
|
||||
|
||||
// ── Chat ──
|
||||
static const chatSendMessage = '/chat/send-message';
|
||||
static const chatHistory = '/chat/history';
|
||||
static const chatSendMessage = '/app/api/chat/send-message';
|
||||
static const chatHistory = '/app/api/chat/history';
|
||||
|
||||
// ── Upload ──
|
||||
static const uploadFile = '/upload/file';
|
||||
static const uploadFile = '/app/api/upload/file';
|
||||
|
||||
// ── WebSocket ──
|
||||
static const wsConnect = '/ws';
|
||||
static const wsConnect = '/websock/open';
|
||||
}
|
||||
|
||||
@@ -7,7 +7,14 @@ class AppConfig {
|
||||
static const isDev = bool.fromEnvironment('IS_DEV', defaultValue: true);
|
||||
static const apiBaseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'https://dev-api.example.com',
|
||||
defaultValue: 'http://gateway.winwayinfo.com',
|
||||
);
|
||||
|
||||
static const channel = String.fromEnvironment('CHANNEL', defaultValue: '10');
|
||||
|
||||
static const appVersion = String.fromEnvironment(
|
||||
'APP_VERSION',
|
||||
defaultValue: '1.0.0',
|
||||
);
|
||||
|
||||
static bool get isProd => !isDev;
|
||||
|
||||
108
apps/im_app/lib/core/foundation/device_info.dart
Normal file
108
apps/im_app/lib/core/foundation/device_info.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
/// 设备 / 运行时信息 — 用于构建 HTTP 请求头中的平台字段
|
||||
///
|
||||
/// 同步 getter(platform / lang / osType)可直接使用。
|
||||
/// deviceId / deviceName 需调用 [init()] 预取后才能通过同步 getter 访问。
|
||||
///
|
||||
/// 在 `AppInitializer.critical` 中调用 [init()],之后 `platformHeaders` 可同步读取所有字段:
|
||||
/// ```dart
|
||||
/// platformHeaders: {
|
||||
/// 'platform': DeviceInfo.platform,
|
||||
/// 'lang': DeviceInfo.lang,
|
||||
/// 'client-version': AppConfig.appVersion,
|
||||
/// 'channel': AppConfig.channel,
|
||||
/// 'device-id': DeviceInfo.deviceId,
|
||||
/// 'device-name': DeviceInfo.deviceName,
|
||||
/// }
|
||||
/// ```
|
||||
// ignore: avoid_classes_with_only_static_members
|
||||
class DeviceInfo {
|
||||
DeviceInfo._();
|
||||
|
||||
static String _deviceId = '';
|
||||
static String _deviceName = '';
|
||||
|
||||
/// 预取设备 ID / 设备名,缓存后可通过同步 getter 访问。
|
||||
/// 在 AppInitializer.critical 中调用一次即可。
|
||||
static Future<void> init() async {
|
||||
_deviceId = await fetchDeviceId();
|
||||
_deviceName = await fetchDeviceName();
|
||||
}
|
||||
|
||||
/// 缓存的设备唯一标识(需先调用 [init()])
|
||||
static String get deviceId => _deviceId;
|
||||
|
||||
/// 缓存的设备名称(需先调用 [init()])
|
||||
static String get deviceName => _deviceName;
|
||||
|
||||
/// HTTP `Platform` 请求头(服务端用于区分来源平台)
|
||||
///
|
||||
/// 返回值:iOS / Android / macOS / Windows / Linux
|
||||
static String get platform {
|
||||
if (Platform.isIOS) return 'iOS';
|
||||
if (Platform.isAndroid) return 'Android';
|
||||
if (Platform.isMacOS) return 'macOS';
|
||||
if (Platform.isWindows) return 'Windows';
|
||||
if (Platform.isLinux) return 'Linux';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/// HTTP `lang` 请求头(取系统首选语言,如 "zh-CN"、"en-US")
|
||||
///
|
||||
/// 使用 `dart:ui` 的 `PlatformDispatcher`,不依赖 Flutter widget tree。
|
||||
static String get lang => PlatformDispatcher.instance.locale.toLanguageTag();
|
||||
|
||||
/// 操作系统类型编号(与服务端约定一致):
|
||||
/// 1=Android 2=iOS 3=Windows 4=macOS 5=Linux 6=其他
|
||||
static int get osType {
|
||||
if (Platform.isAndroid) return 1;
|
||||
if (Platform.isIOS) return 2;
|
||||
if (Platform.isWindows) return 3;
|
||||
if (Platform.isMacOS) return 4;
|
||||
if (Platform.isLinux) return 5;
|
||||
return 6;
|
||||
}
|
||||
|
||||
/// 设备唯一标识
|
||||
///
|
||||
/// Android:Build.ID(非持久化硬件 ID,可作为临时标识;如需稳定 ID 后续接入 android_id)
|
||||
/// iOS:identifierForVendor
|
||||
/// macOS:systemGUID
|
||||
/// Windows:deviceId
|
||||
static Future<String> fetchDeviceId() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
return (await plugin.androidInfo).id;
|
||||
} else if (Platform.isIOS) {
|
||||
return (await plugin.iosInfo).identifierForVendor ?? '';
|
||||
} else if (Platform.isMacOS) {
|
||||
return (await plugin.macOsInfo).systemGUID ?? '';
|
||||
} else if (Platform.isWindows) {
|
||||
return (await plugin.windowsInfo).deviceId;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// 设备名称(品牌 + 型号)
|
||||
static Future<String> fetchDeviceName() async {
|
||||
final plugin = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
final info = await plugin.androidInfo;
|
||||
return '${info.brand} ${info.model}';
|
||||
} else if (Platform.isIOS) {
|
||||
final info = await plugin.iosInfo;
|
||||
return info.utsname.machine;
|
||||
} else if (Platform.isMacOS) {
|
||||
return (await plugin.macOsInfo).model;
|
||||
} else if (Platform.isWindows) {
|
||||
return (await plugin.windowsInfo).computerName;
|
||||
} else if (Platform.isLinux) {
|
||||
return (await plugin.linuxInfo).name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -36,22 +36,9 @@ class ApiErrorCodes {
|
||||
/// 账号在其他设备登录
|
||||
static const int loggedInAnotherDevice = 30006;
|
||||
|
||||
// ── 错误码集合 ──
|
||||
// ── 验证码(30170-30179)──
|
||||
|
||||
/// Token 过期错误码集合 — 触发自动刷新 Token
|
||||
static const Set<int> tokenExpiredCodes = {
|
||||
tokenInvalid,
|
||||
jwtInvalid,
|
||||
sessionInvalid,
|
||||
};
|
||||
/// 触发图片验证:data 含各平台 CAPTCHA token(android / ios / web)
|
||||
static const int captchaRequired = 30174;
|
||||
|
||||
/// 强制登出错误码集合 — 触发退出登录流程
|
||||
static const Set<int> forceLogoutCodes = {refreshTokenFailed};
|
||||
|
||||
/// 踢下线错误码集合 — 触发踢下线 UI 提示
|
||||
static const Set<int> kickOffCodes = {
|
||||
loggedInAnotherDevice,
|
||||
signingMethodError,
|
||||
parsingKeyError,
|
||||
};
|
||||
}
|
||||
|
||||
54
apps/im_app/lib/core/presentation/request_guard.dart
Normal file
54
apps/im_app/lib/core/presentation/request_guard.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
/// ViewModel 请求守卫 mixin
|
||||
///
|
||||
/// 统一拦截 API 调用的 [ApiError],无需在每个 ViewModel 方法内写 try-catch。
|
||||
/// [ApiError.displayMessage] 自动映射为可读文案,直接写入 state。
|
||||
///
|
||||
/// ## 使用
|
||||
///
|
||||
/// ```dart
|
||||
/// @riverpod
|
||||
/// class LoginViewModel extends _$LoginViewModel with RequestGuard<LoginState> {
|
||||
///
|
||||
/// Future<void> sendOtp(String phone) async {
|
||||
/// state = state.copyWith(isLoading: true, error: null);
|
||||
///
|
||||
/// final ok = await guard(
|
||||
/// () => ref.read(loginUseCaseProvider).sendOtp(phone),
|
||||
/// onError: (msg) => state = state.copyWith(isLoading: false, error: msg),
|
||||
/// );
|
||||
///
|
||||
/// if (ok != null) {
|
||||
/// state = state.copyWith(isLoading: false, step: LoginStep.otpSent);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## 机制
|
||||
///
|
||||
/// ```
|
||||
/// guard()
|
||||
/// └─ try: call() → 成功,返回 T
|
||||
/// └─ on ApiError: onError(e.displayMessage) → 返回 null
|
||||
///
|
||||
/// ViewModel: ok != null → 成功路径
|
||||
/// ok == null → 已由 onError 更新 state.error,无需额外处理
|
||||
/// ```
|
||||
mixin RequestGuard<S> on Notifier<S> {
|
||||
/// 执行 [call],捕获 [ApiError] 后调用 [onError] 写入错误文案,返回 null。
|
||||
/// 成功时返回原始结果,ViewModel 用返回值是否为 null 判断走哪条路径。
|
||||
Future<T?> guard<T>(
|
||||
Future<T> Function() call, {
|
||||
required void Function(String message) onError,
|
||||
}) async {
|
||||
try {
|
||||
return await call();
|
||||
} on ApiError catch (e) {
|
||||
onError(e.displayMessage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
|
||||
import 'network_backoff_debouncer.dart';
|
||||
import 'package:im_app/core/services/network_backoff_debouncer.dart';
|
||||
|
||||
/// 消息预处理回调
|
||||
///
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'font.dart';
|
||||
import 'package:im_app/core/ui/base/colors.dart';
|
||||
import 'package:im_app/core/ui/base/font.dart';
|
||||
|
||||
/// 主题组装 -- 将 AppColors / AppFont 组装为 ThemeData
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'font.dart';
|
||||
import 'package:im_app/core/ui/base/font.dart';
|
||||
|
||||
/// 主题样式快捷封装
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../base/context_theme_ext.dart';
|
||||
import 'package:im_app/core/ui/base/context_theme_ext.dart';
|
||||
|
||||
/// # AppButton — 按钮原子组件(L2 Component)
|
||||
///
|
||||
@@ -117,12 +117,9 @@ class AppButton extends StatelessWidget {
|
||||
|
||||
Widget _buildInverse(BuildContext context, Widget label) {
|
||||
final s = context.styles;
|
||||
final isDark = s.isDark;
|
||||
final bg = isDark ? Colors.white : Colors.black;
|
||||
final fg = isDark ? Colors.black : Colors.white;
|
||||
final style = FilledButton.styleFrom(
|
||||
backgroundColor: bg,
|
||||
foregroundColor: fg,
|
||||
backgroundColor: s.onSurface,
|
||||
foregroundColor: s.surface,
|
||||
);
|
||||
if (icon != null) {
|
||||
return FilledButton.icon(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../components/app_button.dart';
|
||||
import 'package:im_app/core/ui/components/app_button.dart';
|
||||
|
||||
/// # AppDialog — 业务确认弹窗(L3 Composite)
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user