ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
264
lib/services/auth_service.dart
Normal file
264
lib/services/auth_service.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../models/user.dart';
|
||||
import '../utils/constants.dart';
|
||||
import '../utils/uuid.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
/// 认证服务:对接真实「爱视后台」/api/sys/* 接口。
|
||||
///
|
||||
/// mock 模式下([MockInterceptor] 挂载)请求路径与真实一致,
|
||||
/// 由拦截器本地返回示例数据,service 层代码两种模式完全统一。
|
||||
class AuthService {
|
||||
AuthService({Dio? dio, Dio? rawDio})
|
||||
: _dio = dio ?? ApiClient.instance.dio,
|
||||
_rawDio = rawDio ?? _createRawDio();
|
||||
|
||||
final Dio _dio;
|
||||
final Dio _rawDio;
|
||||
|
||||
/// 用于拉取验证码图片的独立 Dio 实例(不经过 ApiInterceptor 信封解包)。
|
||||
static Dio _createRawDio() {
|
||||
return Dio(
|
||||
BaseOptions(
|
||||
baseUrl: AppConfig.apiFullBaseUrl,
|
||||
connectTimeout: AppConstants.apiTimeout,
|
||||
receiveTimeout: AppConstants.apiTimeout,
|
||||
responseType: ResponseType.bytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取验证码图片字节与对应 UUID。
|
||||
///
|
||||
/// 接口:GET /captcha.jpg?uuid=xxx,返回图片二进制。
|
||||
/// web 端生成 UUID 规则:[generateUuid]。
|
||||
Future<Captcha> fetchCaptcha() async {
|
||||
final uuid = generateUuid();
|
||||
|
||||
// mock 模式直接返回占位图,不发起真实请求。
|
||||
if (AppConstants.useMock) {
|
||||
return Captcha(uuid: uuid, imageBytes: _mockCaptchaBytes);
|
||||
}
|
||||
|
||||
final response = await _rawDio.get<List<int>>(
|
||||
'/captcha.jpg',
|
||||
queryParameters: {'uuid': uuid},
|
||||
);
|
||||
final bytes = response.data;
|
||||
if (bytes == null || bytes.isEmpty) {
|
||||
throw const AuthException('验证码加载失败');
|
||||
}
|
||||
return Captcha(uuid: uuid, imageBytes: bytes);
|
||||
}
|
||||
|
||||
/// 登录。
|
||||
///
|
||||
/// 请求:POST /sys/login。
|
||||
/// 当 [AppConfig.enableCaptcha] 为真时,body 携带 `{username, password,
|
||||
/// captcha, uuid}`;否则仅发送 `{username, password}`,由后端会话策略决定
|
||||
/// 是否仍需校验验证码(前端已不再展示/收集验证码)。
|
||||
/// 成功后令牌写入 [TokenStore](内存态),失败抛出 [AuthException]。
|
||||
Future<User> login({
|
||||
required String account,
|
||||
required String password,
|
||||
String captcha = '',
|
||||
String uuid = '',
|
||||
}) async {
|
||||
try {
|
||||
// 仅当全局启用验证码时才把 captcha/uuid 填入请求体,
|
||||
// 关闭后登录界面不展示验证码,请求体也不携带这两个字段。
|
||||
final body = <String, dynamic>{
|
||||
'username': account,
|
||||
'password': password,
|
||||
};
|
||||
if (AppConfig.enableCaptcha) {
|
||||
body['captcha'] = captcha;
|
||||
body['uuid'] = uuid;
|
||||
}
|
||||
// 响应已被 ApiInterceptor 解包,data 即业务数据。
|
||||
final response = await _dio.post<dynamic>(
|
||||
'/sys/login',
|
||||
data: body,
|
||||
);
|
||||
final data = response.data;
|
||||
if (data is! Map<String, dynamic> || data['token'] == null) {
|
||||
throw const AuthException('登录响应格式异常');
|
||||
}
|
||||
|
||||
final token = data['token'].toString();
|
||||
TokenStore.instance.save(
|
||||
token: token,
|
||||
refreshToken: data['refreshToken']?.toString(),
|
||||
);
|
||||
|
||||
// 尝试获取当前用户信息以补充显示名称、组织与角色 ID;失败不影响登录。
|
||||
final profile = await _tryFetchUserInfo(fallbackName: account);
|
||||
|
||||
// 登录响应中直接取 orgId/roleId;若不存在则使用 /sys/user/info 结果。
|
||||
final loginOrgId = data['orgId']?.toString();
|
||||
final loginRoleId = data['roleId']?.toString() ??
|
||||
(data['roles'] is List && (data['roles'] as List).isNotEmpty
|
||||
? (data['roles'] as List).first['roleId']?.toString()
|
||||
: null);
|
||||
final loginOrgType = data['orgType'];
|
||||
final loginSuperAdmin = data['superAdmin'];
|
||||
|
||||
return User(
|
||||
id: loginOrgId ?? account,
|
||||
account: account,
|
||||
displayName: profile.displayName,
|
||||
role: _joinRoleNames(data['roleNames']) ?? profile.role ?? '',
|
||||
token: token,
|
||||
orgId: loginOrgId ?? profile.orgId ?? '',
|
||||
roleId: loginRoleId ?? profile.roleId ?? '',
|
||||
orgType: loginOrgType is int
|
||||
? loginOrgType
|
||||
: int.tryParse(loginOrgType?.toString() ?? '') ??
|
||||
profile.orgType,
|
||||
superAdmin: loginSuperAdmin is bool
|
||||
? loginSuperAdmin
|
||||
: profile.superAdmin,
|
||||
);
|
||||
} on AuthException {
|
||||
rethrow;
|
||||
} on DioException catch (e) {
|
||||
throw AuthException(_errorMessage(e, fallback: '账号或密码错误'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 登出:通知后端(失败忽略),并清空内存令牌。
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
await _dio.post<dynamic>('/sys/logout');
|
||||
} catch (_) {
|
||||
// 登出接口失败不阻塞本地登出。
|
||||
} finally {
|
||||
TokenStore.instance.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 拉取用户资料(GET /sys/user/info);失败时返回兜底值。
|
||||
Future<_UserProfile> _tryFetchUserInfo({required String fallbackName}) async {
|
||||
try {
|
||||
final response = await _dio.get<dynamic>('/sys/user/info');
|
||||
final data = response.data;
|
||||
if (data is Map<String, dynamic>) {
|
||||
final roleId = data['roleId']?.toString() ??
|
||||
_extractFirstRoleId(data['roles'] ?? data['role']);
|
||||
final orgId = data['orgId']?.toString() ??
|
||||
_extractNestedId(data['org'] ?? data['dept']);
|
||||
final rawOrgType = data['orgType'];
|
||||
final rawSuperAdmin = data['superAdmin'];
|
||||
return _UserProfile(
|
||||
displayName: (data['realName'] ??
|
||||
data['nickname'] ??
|
||||
data['username'] ??
|
||||
fallbackName)
|
||||
.toString(),
|
||||
role: _joinRoleNames(data['roleNames']),
|
||||
orgId: orgId,
|
||||
roleId: roleId,
|
||||
orgType: rawOrgType is int
|
||||
? rawOrgType
|
||||
: int.tryParse(rawOrgType?.toString() ?? '') ?? 0,
|
||||
superAdmin: rawSuperAdmin is bool ? rawSuperAdmin : false,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// 用户信息接口异常时使用兜底资料。
|
||||
}
|
||||
return _UserProfile(displayName: fallbackName, role: null);
|
||||
}
|
||||
|
||||
/// 从角色对象或列表中提取第一个 roleId。
|
||||
static String? _extractFirstRoleId(dynamic value) {
|
||||
if (value is List && value.isNotEmpty) {
|
||||
return _extractFirstRoleId(value.first);
|
||||
}
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value['roleId']?.toString() ?? value['id']?.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 从组织/部门对象中提取 ID。
|
||||
static String? _extractNestedId(dynamic value) {
|
||||
if (value is Map<String, dynamic>) {
|
||||
return value['orgId']?.toString() ??
|
||||
value['id']?.toString() ??
|
||||
value['deptId']?.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// roleNames 列表拼接为展示字符串。
|
||||
static String? _joinRoleNames(dynamic roleNames) {
|
||||
if (roleNames is List && roleNames.isNotEmpty) {
|
||||
return roleNames.map((e) => e.toString()).join('/');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 从 Dio 异常中提取业务错误信息。
|
||||
static String _errorMessage(DioException e, {required String fallback}) {
|
||||
final error = e.error;
|
||||
if (error is ApiException && error.message.isNotEmpty) {
|
||||
return error.message;
|
||||
}
|
||||
if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout ||
|
||||
e.type == DioExceptionType.connectionError) {
|
||||
return '网络异常,请检查网络后重试';
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// mock 模式下的占位验证码图片:1x1 透明 PNG。
|
||||
static final List<int> _mockCaptchaBytes = base64Decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
|
||||
);
|
||||
}
|
||||
|
||||
/// 用户资料中间结构(service 内部使用)。
|
||||
class _UserProfile {
|
||||
const _UserProfile({
|
||||
required this.displayName,
|
||||
this.role,
|
||||
this.orgId,
|
||||
this.roleId,
|
||||
this.orgType = 0,
|
||||
this.superAdmin = false,
|
||||
});
|
||||
|
||||
final String displayName;
|
||||
final String? role;
|
||||
final String? orgId;
|
||||
final String? roleId;
|
||||
final int orgType;
|
||||
final bool superAdmin;
|
||||
}
|
||||
|
||||
/// 验证码数据。
|
||||
class Captcha {
|
||||
const Captcha({required this.uuid, required this.imageBytes});
|
||||
|
||||
/// 与该验证码图片绑定的 UUID,登录时必须原样回传。
|
||||
final String uuid;
|
||||
|
||||
/// 验证码 PNG/JPEG 图片字节。
|
||||
final List<int> imageBytes;
|
||||
}
|
||||
|
||||
/// 认证异常。
|
||||
class AuthException implements Exception {
|
||||
const AuthException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
Reference in New Issue
Block a user