网络请求打通,ws 打通

This commit is contained in:
Cody
2026-03-09 19:05:55 +08:00
parent 997d821447
commit 3c1976b343
60 changed files with 1392 additions and 552 deletions

View File

@@ -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';
}

View File

@@ -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;

View File

@@ -0,0 +1,108 @@
import 'dart:io';
import 'dart:ui';
import 'package:device_info_plus/device_info_plus.dart';
/// 设备 / 运行时信息 — 用于构建 HTTP 请求头中的平台字段
///
/// 同步 getterplatform / 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;
}
/// 设备唯一标识
///
/// AndroidBuild.ID非持久化硬件 ID可作为临时标识如需稳定 ID 后续接入 android_id
/// iOSidentifierForVendor
/// macOSsystemGUID
/// WindowsdeviceId
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 '';
}
}

View File

@@ -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 tokenandroid / ios / web
static const int captchaRequired = 30174;
/// 强制登出错误码集合 — 触发退出登录流程
static const Set<int> forceLogoutCodes = {refreshTokenFailed};
/// 踢下线错误码集合 — 触发踢下线 UI 提示
static const Set<int> kickOffCodes = {
loggedInAnotherDevice,
signingMethodError,
parsingKeyError,
};
}

View 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;
}
}
}

View File

@@ -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';
/// 消息预处理回调
///

View File

@@ -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
///

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'font.dart';
import 'package:im_app/core/ui/base/font.dart';
/// 主题样式快捷封装
///

View File

@@ -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(

View File

@@ -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
///