😋 初始化仓库
This commit is contained in:
2
lib/utils/.gitkeep
Normal file
2
lib/utils/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Utils directory
|
||||
# This directory contains utility functions and helpers
|
||||
239
lib/utils/app_logger.dart
Normal file
239
lib/utils/app_logger.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// Application-wide logger with configurable levels and output
|
||||
class AppLogger {
|
||||
static final AppLogger _instance = AppLogger._internal();
|
||||
factory AppLogger() => _instance;
|
||||
|
||||
late final Logger _logger;
|
||||
bool _isInitialized = false;
|
||||
|
||||
AppLogger._internal();
|
||||
|
||||
/// Initialize the logger with appropriate settings
|
||||
void initialize({Level? level, LogOutput? output, LogFilter? filter}) {
|
||||
if (_isInitialized) return;
|
||||
|
||||
_logger = Logger(
|
||||
level: level ?? _getDefaultLevel(),
|
||||
filter: filter ?? ProductionFilter(),
|
||||
printer: _createPrinter(),
|
||||
output: output ?? _createOutput(),
|
||||
);
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// Get default log level based on build mode
|
||||
Level _getDefaultLevel() {
|
||||
if (kDebugMode) {
|
||||
return Level.debug;
|
||||
} else if (kProfileMode) {
|
||||
return Level.info;
|
||||
} else {
|
||||
return Level.warning;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create appropriate printer based on build mode
|
||||
LogPrinter _createPrinter() {
|
||||
if (kDebugMode) {
|
||||
// Detailed printer for development
|
||||
return PrettyPrinter(
|
||||
methodCount: 2,
|
||||
errorMethodCount: 8,
|
||||
lineLength: 120,
|
||||
colors: true,
|
||||
printEmojis: true,
|
||||
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
||||
);
|
||||
} else {
|
||||
// Simple printer for production
|
||||
return SimplePrinter(colors: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create appropriate output based on build mode
|
||||
LogOutput _createOutput() {
|
||||
if (kReleaseMode) {
|
||||
// In production, you might want to send logs to a file or remote service
|
||||
return ConsoleOutput();
|
||||
} else {
|
||||
return ConsoleOutput();
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure logger is initialized
|
||||
void _ensureInitialized() {
|
||||
if (!_isInitialized) {
|
||||
initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a trace message (most verbose)
|
||||
void trace(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.t(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log a debug message
|
||||
void debug(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.d(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log an info message
|
||||
void info(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.i(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log a warning message
|
||||
void warning(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.w(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log an error message
|
||||
void error(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.e(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log a fatal error message
|
||||
void fatal(String message, {Object? error, StackTrace? stackTrace}) {
|
||||
_ensureInitialized();
|
||||
_logger.f(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
/// Log network request
|
||||
void logRequest(
|
||||
String method,
|
||||
String url, {
|
||||
Map<String, dynamic>? headers,
|
||||
dynamic body,
|
||||
}) {
|
||||
if (!kReleaseMode) {
|
||||
_ensureInitialized();
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('→ $method $url');
|
||||
|
||||
if (headers != null && headers.isNotEmpty) {
|
||||
buffer.writeln('Headers: ${_sanitizeHeaders(headers)}');
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('Body: ${_sanitizeBody(body)}');
|
||||
}
|
||||
|
||||
_logger.d(buffer.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Log network response
|
||||
void logResponse(
|
||||
String method,
|
||||
String url,
|
||||
int statusCode, {
|
||||
dynamic body,
|
||||
Duration? duration,
|
||||
}) {
|
||||
if (!kReleaseMode) {
|
||||
_ensureInitialized();
|
||||
final buffer = StringBuffer();
|
||||
buffer.write('← $method $url [$statusCode]');
|
||||
|
||||
if (duration != null) {
|
||||
buffer.write(' (${duration.inMilliseconds}ms)');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
if (body != null) {
|
||||
buffer.writeln('Body: ${_sanitizeBody(body)}');
|
||||
}
|
||||
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
_logger.d(buffer.toString());
|
||||
} else if (statusCode >= 400) {
|
||||
_logger.e(buffer.toString());
|
||||
} else {
|
||||
_logger.w(buffer.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitize headers to remove sensitive information
|
||||
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
|
||||
final sanitized = Map<String, dynamic>.from(headers);
|
||||
|
||||
// List of sensitive header keys to redact
|
||||
const sensitiveKeys = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'set-cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
|
||||
for (final key in sensitiveKeys) {
|
||||
if (sanitized.containsKey(key.toLowerCase())) {
|
||||
sanitized[key] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/// Sanitize body to remove sensitive information
|
||||
dynamic _sanitizeBody(dynamic body) {
|
||||
if (body is Map) {
|
||||
final sanitized = Map<String, dynamic>.from(body);
|
||||
|
||||
// List of sensitive field names to redact
|
||||
const sensitiveFields = [
|
||||
'password',
|
||||
'pwd',
|
||||
'passwd',
|
||||
'token',
|
||||
'secret',
|
||||
'apiKey',
|
||||
'api_key',
|
||||
];
|
||||
|
||||
for (final field in sensitiveFields) {
|
||||
if (sanitized.containsKey(field)) {
|
||||
sanitized[field] = '***REDACTED***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// For string bodies, check if it contains sensitive patterns
|
||||
if (body is String) {
|
||||
if (body.length > 1000) {
|
||||
return '${body.substring(0, 1000)}... (truncated)';
|
||||
}
|
||||
|
||||
// Redact potential passwords in query strings or form data
|
||||
return body.replaceAllMapped(
|
||||
RegExp(
|
||||
r'(password|pwd|passwd|token|secret)=[^&\s]+',
|
||||
caseSensitive: false,
|
||||
),
|
||||
(match) => '${match.group(1)}=***REDACTED***',
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/// Close the logger and release resources
|
||||
void close() {
|
||||
if (_isInitialized) {
|
||||
_logger.close();
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
lib/utils/error_handler.dart
Normal file
174
lib/utils/error_handler.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'app_logger.dart';
|
||||
import 'exceptions.dart';
|
||||
|
||||
/// Global error handler for the application
|
||||
class ErrorHandler {
|
||||
static final ErrorHandler _instance = ErrorHandler._internal();
|
||||
factory ErrorHandler() => _instance;
|
||||
ErrorHandler._internal();
|
||||
|
||||
final AppLogger _logger = AppLogger();
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// Initialize global error handling
|
||||
void initialize() {
|
||||
if (_isInitialized) return;
|
||||
|
||||
// Capture Flutter framework errors
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
_handleFlutterError(details);
|
||||
};
|
||||
|
||||
// Capture errors in platform-specific code
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
_handlePlatformError(error, stack);
|
||||
return true;
|
||||
};
|
||||
|
||||
_isInitialized = true;
|
||||
_logger.info('Global error handler initialized');
|
||||
}
|
||||
|
||||
/// Run the app with error zone guarding
|
||||
static Future<void> runAppWithErrorHandling(
|
||||
Future<void> Function() appRunner,
|
||||
) async {
|
||||
final errorHandler = ErrorHandler();
|
||||
errorHandler.initialize();
|
||||
|
||||
// Run app in a guarded zone to catch async errors
|
||||
await runZonedGuarded(
|
||||
() async {
|
||||
await appRunner();
|
||||
},
|
||||
(error, stackTrace) {
|
||||
errorHandler._handleZoneError(error, stackTrace);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle Flutter framework errors
|
||||
void _handleFlutterError(FlutterErrorDetails details) {
|
||||
// Log the error
|
||||
_logger.error(
|
||||
'Flutter Error',
|
||||
error: details.exception,
|
||||
stackTrace: details.stack,
|
||||
);
|
||||
|
||||
// In debug mode, show the red error screen
|
||||
if (kDebugMode) {
|
||||
FlutterError.presentError(details);
|
||||
}
|
||||
|
||||
// Optionally report to crash reporting service
|
||||
_reportError(details.exception, details.stack);
|
||||
}
|
||||
|
||||
/// Handle platform-specific errors
|
||||
bool _handlePlatformError(Object error, StackTrace stackTrace) {
|
||||
_logger.error('Platform Error', error: error, stackTrace: stackTrace);
|
||||
|
||||
_reportError(error, stackTrace);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Handle errors from runZonedGuarded
|
||||
void _handleZoneError(Object error, StackTrace stackTrace) {
|
||||
_logger.error('Async Error', error: error, stackTrace: stackTrace);
|
||||
|
||||
_reportError(error, stackTrace);
|
||||
}
|
||||
|
||||
/// Report error to external service (optional)
|
||||
void _reportError(Object error, StackTrace? stackTrace) {
|
||||
// In production, you could send errors to services like:
|
||||
// - Firebase Crashlytics
|
||||
// - Sentry
|
||||
// - Custom error reporting endpoint
|
||||
|
||||
if (kReleaseMode) {
|
||||
// TODO: Implement error reporting to external service
|
||||
// Example: FirebaseCrashlytics.instance.recordError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle and display user-friendly error messages
|
||||
static String getUserFriendlyMessage(Object error) {
|
||||
if (error is AppException) {
|
||||
return error.message;
|
||||
} else if (error is NetworkException) {
|
||||
return '网络连接失败,请检查网络设置';
|
||||
} else if (error is AuthenticationException) {
|
||||
return '登录失败,请检查账号密码';
|
||||
} else if (error is ParseException) {
|
||||
return '数据解析失败,请稍后重试';
|
||||
} else if (error is ValidationException) {
|
||||
return '输入验证失败:${error.message}';
|
||||
} else if (error is TimeoutException) {
|
||||
return '请求超时,请稍后重试';
|
||||
} else if (error is FormatException) {
|
||||
return '数据格式错误';
|
||||
} else {
|
||||
return '发生未知错误,请稍后重试';
|
||||
}
|
||||
}
|
||||
|
||||
/// Show error dialog to user
|
||||
static void showErrorDialog(
|
||||
BuildContext context,
|
||||
Object error, {
|
||||
VoidCallback? onRetry,
|
||||
}) {
|
||||
final message = getUserFriendlyMessage(error);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('错误'),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
if (onRetry != null)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onRetry();
|
||||
},
|
||||
child: const Text('重试'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show error snackbar to user
|
||||
static void showErrorSnackBar(
|
||||
BuildContext context,
|
||||
Object error, {
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
final message = getUserFriendlyMessage(error);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: duration,
|
||||
backgroundColor: Colors.red,
|
||||
action: SnackBarAction(
|
||||
label: '关闭',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
148
lib/utils/exceptions.dart
Normal file
148
lib/utils/exceptions.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
/// Base exception class for all application exceptions
|
||||
abstract class AppException implements Exception {
|
||||
final String message;
|
||||
final String? details;
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
AppException(this.message, [this.details, this.stackTrace]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (details != null) {
|
||||
return '$runtimeType: $message\nDetails: $details';
|
||||
}
|
||||
return '$runtimeType: $message';
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception thrown when network operations fail
|
||||
class NetworkException extends AppException {
|
||||
final int? statusCode;
|
||||
final String? url;
|
||||
|
||||
NetworkException(
|
||||
super.message, [
|
||||
super.details,
|
||||
super.stackTrace,
|
||||
this.statusCode,
|
||||
this.url,
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer('NetworkException: $message');
|
||||
if (statusCode != null) {
|
||||
buffer.write('\nStatus Code: $statusCode');
|
||||
}
|
||||
if (url != null) {
|
||||
buffer.write('\nURL: $url');
|
||||
}
|
||||
if (details != null) {
|
||||
buffer.write('\nDetails: $details');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception thrown when authentication fails
|
||||
class AuthenticationException extends AppException {
|
||||
final String? userId;
|
||||
final AuthenticationFailureReason? reason;
|
||||
|
||||
AuthenticationException(
|
||||
super.message, [
|
||||
super.details,
|
||||
super.stackTrace,
|
||||
this.userId,
|
||||
this.reason,
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer('AuthenticationException: $message');
|
||||
if (userId != null) {
|
||||
buffer.write('\nUser ID: $userId');
|
||||
}
|
||||
if (reason != null) {
|
||||
buffer.write('\nReason: ${reason!.name}');
|
||||
}
|
||||
if (details != null) {
|
||||
buffer.write('\nDetails: $details');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons for authentication failure
|
||||
enum AuthenticationFailureReason {
|
||||
invalidCredentials,
|
||||
sessionExpired,
|
||||
networkError,
|
||||
serverError,
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Exception thrown when parsing fails
|
||||
class ParseException extends AppException {
|
||||
final String? source;
|
||||
final String? expectedFormat;
|
||||
|
||||
ParseException(
|
||||
super.message, [
|
||||
super.details,
|
||||
super.stackTrace,
|
||||
this.source,
|
||||
this.expectedFormat,
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer('ParseException: $message');
|
||||
if (expectedFormat != null) {
|
||||
buffer.write('\nExpected Format: $expectedFormat');
|
||||
}
|
||||
if (source != null && source!.length <= 100) {
|
||||
buffer.write('\nSource: $source');
|
||||
} else if (source != null) {
|
||||
buffer.write('\nSource: ${source!.substring(0, 100)}...');
|
||||
}
|
||||
if (details != null) {
|
||||
buffer.write('\nDetails: $details');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception thrown when validation fails
|
||||
class ValidationException extends AppException {
|
||||
final String? fieldName;
|
||||
final dynamic invalidValue;
|
||||
final List<String>? validationRules;
|
||||
|
||||
ValidationException(
|
||||
super.message, [
|
||||
super.details,
|
||||
super.stackTrace,
|
||||
this.fieldName,
|
||||
this.invalidValue,
|
||||
this.validationRules,
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final buffer = StringBuffer('ValidationException: $message');
|
||||
if (fieldName != null) {
|
||||
buffer.write('\nField: $fieldName');
|
||||
}
|
||||
if (invalidValue != null) {
|
||||
buffer.write('\nInvalid Value: $invalidValue');
|
||||
}
|
||||
if (validationRules != null && validationRules!.isNotEmpty) {
|
||||
buffer.write('\nValidation Rules: ${validationRules!.join(", ")}');
|
||||
}
|
||||
if (details != null) {
|
||||
buffer.write('\nDetails: $details');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
76
lib/utils/retry_handler.dart
Normal file
76
lib/utils/retry_handler.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// 重试处理器,支持指数退避策略
|
||||
class RetryHandler {
|
||||
static const int maxRetries = 3;
|
||||
static const Duration initialDelay = Duration(seconds: 1);
|
||||
static const double exponentialBase = 2.0;
|
||||
|
||||
/// 执行带重试的异步操作
|
||||
///
|
||||
/// [operation] 要执行的异步操作
|
||||
/// [retryIf] 可选的条件函数,返回true时才重试
|
||||
/// [maxAttempts] 最大尝试次数,默认为3次
|
||||
/// [onRetry] 可选的重试回调,参数为当前尝试次数和错误
|
||||
static Future<T> retry<T>({
|
||||
required Future<T> Function() operation,
|
||||
bool Function(dynamic error)? retryIf,
|
||||
int maxAttempts = maxRetries,
|
||||
void Function(int attempt, dynamic error)? onRetry,
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
dynamic lastError;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
attempt++;
|
||||
return await operation();
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
|
||||
// 检查是否应该重试
|
||||
if (attempt >= maxAttempts || (retryIf != null && !retryIf(e))) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
// 计算延迟时间(指数退避)
|
||||
final delay = initialDelay * pow(exponentialBase, attempt - 1);
|
||||
|
||||
// 调用重试回调
|
||||
if (onRetry != null) {
|
||||
onRetry(attempt, e);
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断错误是否应该重试(网络相关错误)
|
||||
static bool shouldRetryOnError(dynamic error) {
|
||||
// 可以根据具体错误类型判断是否应该重试
|
||||
// 例如:网络超时、连接失败等应该重试
|
||||
// 认证失败、参数错误等不应该重试
|
||||
final errorStr = error.toString().toLowerCase();
|
||||
|
||||
// 应该重试的错误类型
|
||||
if (errorStr.contains('timeout') ||
|
||||
errorStr.contains('connection') ||
|
||||
errorStr.contains('network') ||
|
||||
errorStr.contains('socket')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不应该重试的错误类型
|
||||
if (errorStr.contains('authentication') ||
|
||||
errorStr.contains('unauthorized') ||
|
||||
errorStr.contains('forbidden') ||
|
||||
errorStr.contains('invalid')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 默认重试
|
||||
return true;
|
||||
}
|
||||
}
|
||||
263
lib/utils/session_manager.dart
Normal file
263
lib/utils/session_manager.dart
Normal file
@@ -0,0 +1,263 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../models/user_credentials.dart';
|
||||
import '../providers/auth_provider.dart';
|
||||
|
||||
/// Session manager for handling app startup and session restoration
|
||||
///
|
||||
/// Provides utilities for:
|
||||
/// - Checking if saved credentials exist
|
||||
/// - Attempting to restore previous session
|
||||
/// - Handling session expiration
|
||||
///
|
||||
/// Usage example:
|
||||
/// ```dart
|
||||
/// final sessionManager = SessionManager(authProvider: authProvider);
|
||||
///
|
||||
/// // Check if session can be restored
|
||||
/// final canRestore = await sessionManager.canRestoreSession();
|
||||
///
|
||||
/// // Attempt to restore session
|
||||
/// final restored = await sessionManager.restoreSession();
|
||||
///
|
||||
/// // Handle session expiration
|
||||
/// await sessionManager.handleSessionExpired();
|
||||
/// ```
|
||||
class SessionManager {
|
||||
final AuthProvider _authProvider;
|
||||
|
||||
SessionManager({required AuthProvider authProvider})
|
||||
: _authProvider = authProvider;
|
||||
|
||||
/// Check if saved credentials exist
|
||||
///
|
||||
/// Returns true if credentials are stored, false otherwise
|
||||
/// Does not validate if the session is still active
|
||||
Future<bool> hasSavedCredentials() async {
|
||||
try {
|
||||
final credentials = await UserCredentials.loadSecurely();
|
||||
return credentials != null;
|
||||
} catch (e) {
|
||||
debugPrint('Error checking saved credentials: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if session can be restored
|
||||
///
|
||||
/// Checks if saved credentials exist and are valid
|
||||
/// Returns true if session restoration should be attempted
|
||||
Future<bool> canRestoreSession() async {
|
||||
return await hasSavedCredentials();
|
||||
}
|
||||
|
||||
/// Attempt to restore session from saved credentials
|
||||
///
|
||||
/// Loads credentials from secure storage and attempts login
|
||||
/// Returns SessionRestoreResult with status and details
|
||||
Future<SessionRestoreResult> restoreSession() async {
|
||||
try {
|
||||
// Check if credentials exist
|
||||
final hasCredentials = await hasSavedCredentials();
|
||||
if (!hasCredentials) {
|
||||
return SessionRestoreResult(
|
||||
success: false,
|
||||
reason: SessionRestoreFailureReason.noCredentials,
|
||||
message: '未找到保存的登录凭证',
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to restore session using AuthProvider
|
||||
final restored = await _authProvider.restoreSession();
|
||||
|
||||
if (restored) {
|
||||
return SessionRestoreResult(success: true, message: '会话恢复成功');
|
||||
} else {
|
||||
// Check the error message from auth provider
|
||||
final errorMessage = _authProvider.errorMessage;
|
||||
final reason = _determineFailureReason(errorMessage);
|
||||
|
||||
return SessionRestoreResult(
|
||||
success: false,
|
||||
reason: reason,
|
||||
message: errorMessage ?? '会话恢复失败',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error restoring session: $e');
|
||||
return SessionRestoreResult(
|
||||
success: false,
|
||||
reason: SessionRestoreFailureReason.unknown,
|
||||
message: '会话恢复出错: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle session expiration
|
||||
///
|
||||
/// Clears current session and credentials
|
||||
/// Should be called when session is detected as expired
|
||||
Future<void> handleSessionExpired() async {
|
||||
try {
|
||||
await _authProvider.logout();
|
||||
debugPrint('Session expired and cleared');
|
||||
} catch (e) {
|
||||
debugPrint('Error handling session expiration: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear saved session data
|
||||
///
|
||||
/// Removes all saved credentials and session information
|
||||
/// Useful for logout or when user wants to clear data
|
||||
Future<void> clearSession() async {
|
||||
try {
|
||||
await _authProvider.logout();
|
||||
debugPrint('Session cleared successfully');
|
||||
} catch (e) {
|
||||
debugPrint('Error clearing session: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate current session
|
||||
///
|
||||
/// Checks if the current session is still valid
|
||||
/// Returns true if session is active and healthy
|
||||
Future<bool> validateSession() async {
|
||||
try {
|
||||
if (!_authProvider.isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _authProvider.checkSession();
|
||||
} catch (e) {
|
||||
debugPrint('Error validating session: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get session status
|
||||
///
|
||||
/// Returns current session status information
|
||||
SessionStatus getSessionStatus() {
|
||||
return SessionStatus(
|
||||
isAuthenticated: _authProvider.isAuthenticated,
|
||||
authState: _authProvider.state,
|
||||
hasConnection: _authProvider.connection != null,
|
||||
errorMessage: _authProvider.errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/// Determine failure reason from error message
|
||||
SessionRestoreFailureReason _determineFailureReason(String? errorMessage) {
|
||||
if (errorMessage == null) {
|
||||
return SessionRestoreFailureReason.unknown;
|
||||
}
|
||||
|
||||
if (errorMessage.contains('密码错误') || errorMessage.contains('凭证')) {
|
||||
return SessionRestoreFailureReason.invalidCredentials;
|
||||
} else if (errorMessage.contains('网络') || errorMessage.contains('连接')) {
|
||||
return SessionRestoreFailureReason.networkError;
|
||||
} else if (errorMessage.contains('过期')) {
|
||||
return SessionRestoreFailureReason.sessionExpired;
|
||||
} else {
|
||||
return SessionRestoreFailureReason.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of session restoration attempt
|
||||
class SessionRestoreResult {
|
||||
final bool success;
|
||||
final SessionRestoreFailureReason? reason;
|
||||
final String message;
|
||||
|
||||
SessionRestoreResult({
|
||||
required this.success,
|
||||
this.reason,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SessionRestoreResult(success: $success, reason: $reason, message: $message)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons why session restoration might fail
|
||||
enum SessionRestoreFailureReason {
|
||||
/// No saved credentials found
|
||||
noCredentials,
|
||||
|
||||
/// Saved credentials are invalid
|
||||
invalidCredentials,
|
||||
|
||||
/// Session has expired
|
||||
sessionExpired,
|
||||
|
||||
/// Network connection error
|
||||
networkError,
|
||||
|
||||
/// Unknown error
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Current session status information
|
||||
class SessionStatus {
|
||||
final bool isAuthenticated;
|
||||
final AuthState authState;
|
||||
final bool hasConnection;
|
||||
final String? errorMessage;
|
||||
|
||||
SessionStatus({
|
||||
required this.isAuthenticated,
|
||||
required this.authState,
|
||||
required this.hasConnection,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Check if session is healthy
|
||||
bool get isHealthy =>
|
||||
isAuthenticated && hasConnection && errorMessage == null;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SessionStatus('
|
||||
'isAuthenticated: $isAuthenticated, '
|
||||
'authState: $authState, '
|
||||
'hasConnection: $hasConnection, '
|
||||
'errorMessage: $errorMessage'
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods for SessionRestoreFailureReason
|
||||
extension SessionRestoreFailureReasonExtension on SessionRestoreFailureReason {
|
||||
/// Get user-friendly message for the failure reason
|
||||
String get userMessage {
|
||||
switch (this) {
|
||||
case SessionRestoreFailureReason.noCredentials:
|
||||
return '未找到保存的登录信息,请重新登录';
|
||||
case SessionRestoreFailureReason.invalidCredentials:
|
||||
return '登录凭证无效,请重新登录';
|
||||
case SessionRestoreFailureReason.sessionExpired:
|
||||
return '会话已过期,请重新登录';
|
||||
case SessionRestoreFailureReason.networkError:
|
||||
return '网络连接失败,请检查网络后重试';
|
||||
case SessionRestoreFailureReason.unknown:
|
||||
return '会话恢复失败,请重新登录';
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if user should be prompted to login again
|
||||
bool get shouldPromptLogin {
|
||||
switch (this) {
|
||||
case SessionRestoreFailureReason.noCredentials:
|
||||
case SessionRestoreFailureReason.invalidCredentials:
|
||||
case SessionRestoreFailureReason.sessionExpired:
|
||||
return true;
|
||||
case SessionRestoreFailureReason.networkError:
|
||||
case SessionRestoreFailureReason.unknown:
|
||||
return false; // User might want to retry
|
||||
}
|
||||
}
|
||||
}
|
||||
194
lib/utils/text_generator.dart
Normal file
194
lib/utils/text_generator.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
import 'dart:math';
|
||||
import '../models/questionnaire.dart';
|
||||
|
||||
/// Text generator for evaluation questionnaires
|
||||
/// Generates appropriate text responses based on question type
|
||||
class TextGenerator {
|
||||
static final Random _random = Random();
|
||||
|
||||
// 启发类文案库
|
||||
static const List<String> inspirationTexts = [
|
||||
"老师认真负责的态度和丰富的讲课内容,让我明白了扎实的知识积累对学习的重要性",
|
||||
"老师能够深入了解学生的学习状况,启发我学会了关注细节、因材施教的道理",
|
||||
"老师授课有条理有重点,教会我做事要分清主次、抓住关键的思维方法",
|
||||
"老师善于用凝练的语言表达复杂内容,让我学会了如何提炼要点、化繁为简",
|
||||
"老师对深奥现象解释得通俗易懂,启发我认识到深入浅出是一种重要的能力",
|
||||
"老师采用多种教学方式让学生更好接受知识,让我明白了方法灵活运用的重要性",
|
||||
"老师既严格要求又鼓励学生发言,教会我严慈相济、宽严并济的处事原则",
|
||||
"老师能够调动学生的积极性,启发我懂得了激发他人潜能和主动性的价值",
|
||||
"老师课堂气氛活跃但不失严谨,让我理解了轻松与高效可以兼得的道理",
|
||||
"老师治学严谨、循循善诱的风格,激励我要保持谦逊认真的学习态度和钻研精神",
|
||||
"老师对学科的热爱和投入,让我感受到保持热情对做好任何事情的重要意义",
|
||||
"老师善于联系实际讲解理论知识,启发我学会了理论联系实际的思维方式",
|
||||
"老师注重培养学生的自主学习能力,让我明白了授人以渔的教育真谛",
|
||||
"老师对每个问题的耐心解答,教会我做事要有耐心和责任心",
|
||||
"老师在课堂上的幽默感,让我懂得了适度的轻松能够提高工作和学习效率",
|
||||
"老师严格的课堂管理,启发我认识到纪律和规则对集体活动的重要性",
|
||||
"老师丰富的专业知识储备,激励我要不断充实自己、拓宽知识面",
|
||||
"老师对学生的一视同仁,让我理解了公平公正待人的重要价值",
|
||||
"老师善于鼓励和肯定学生,教会我正面激励对他人成长的积极作用",
|
||||
"老师清晰的逻辑思维,启发我学会了有条理地思考和表达问题",
|
||||
"老师对教学的精心准备,让我明白了充分准备是做好工作的前提",
|
||||
"老师善于归纳总结重点,教会我抓住事物本质和核心的思维能力",
|
||||
"老师对学生问题的重视,启发我懂得了倾听和尊重他人意见的重要性",
|
||||
"老师灵活的教学节奏把握,让我学会了根据实际情况灵活调整的智慧",
|
||||
"老师富有感染力的授课方式,教会我热情和真诚能够打动他人",
|
||||
"老师注重学生的全面发展,启发我认识到综合素质培养的重要性",
|
||||
"老师对细节的关注,让我明白了细节决定成败的道理",
|
||||
"老师善于启发学生独立思考,教会我批判性思维和质疑精神的可贵",
|
||||
"老师持续学习、与时俱进的态度,激励我要保持终身学习的理念",
|
||||
"老师对学生的关心和帮助,让我理解了教书育人、为人师表的深刻内涵",
|
||||
];
|
||||
|
||||
// 建议类文案库
|
||||
static const List<String> suggestionTexts = [
|
||||
'无',
|
||||
'没有',
|
||||
"老师讲课很好,很认真负责,我没有什么建议,希望老师继续保持现有的教学方式",
|
||||
"老师授课认真,课堂效率高,我觉得一切都很好,暂时没有什么意见和建议",
|
||||
"老师上课既幽默又严格,教学方法很适合我们,没有需要改进的地方",
|
||||
"老师治学严谨,循循善诱,对老师的授课我非常满意,请老师保持这种教学状态",
|
||||
"老师授课有条理有重点,我认为已经做得很到位了,没有什么建议可提",
|
||||
"老师善于用凝练的语言讲解复杂内容,教学方式很好,希望老师继续发扬优点",
|
||||
"老师讲课内容详细,条理清晰,我觉得没有什么需要调整的地方,一切都很棒",
|
||||
"老师讲授认真,内容丰富,我对教学方式非常认可,请老师保持现在的风格",
|
||||
"老师对待教学认真负责,能够调动学生积极性,我没有什么意见,希望老师继续保持",
|
||||
"老师课堂效率高,气氛活跃,整节课学下来很有收获,暂时想不到需要改进的地方",
|
||||
"老师教学态度端正,讲课思路清晰,我觉得非常好,没有什么意见和建议",
|
||||
"老师授课生动有趣,深入浅出,对老师的教学我很满意,请老师保持下去",
|
||||
"老师对学生要求严格但不失关怀,教学方法得当,我没有什么建议可提",
|
||||
"老师讲课重点突出,内容充实,我认为一切都很好,希望老师继续保持",
|
||||
"老师课堂互动性强,能照顾到每个学生,我觉得没有需要改进的地方",
|
||||
"老师备课充分,讲解透彻,对老师的授课非常认可,暂时没有什么意见",
|
||||
"老师教学经验丰富,方法多样,我觉得已经很优秀了,请老师保持现状",
|
||||
"老师语言表达清晰,逻辑性强,我没有什么建议,希望老师继续发扬",
|
||||
"老师授课节奏把握得很好,我认为非常合适,没有什么需要调整的",
|
||||
"老师对待学生耐心负责,教学效果显著,我很满意,请老师保持",
|
||||
"老师讲课富有激情,能感染学生,我觉得很好,暂时没有什么意见",
|
||||
"老师专业知识扎实,讲解到位,对老师的教学我非常认可,没有建议",
|
||||
"老师善于引导学生思考,启发性强,我认为一切都很好,请老师保持",
|
||||
"老师课堂管理有序,教学效率高,我觉得没有什么需要改进的地方",
|
||||
"老师授课风格独特,深受学生喜爱,我没有什么意见和建议",
|
||||
"老师讲课深入浅出,通俗易懂,我认为非常好,希望老师继续保持",
|
||||
"老师对学生一视同仁,公平公正,我很满意老师的教学方式",
|
||||
"老师教学方法科学合理,效果突出,我觉得没有需要调整的地方",
|
||||
"老师认真批改作业,及时反馈,对老师的工作我非常认可,请保持",
|
||||
"老师课堂内容丰富多彩,讲解细致入微,我没有什么建议,一切都很好",
|
||||
];
|
||||
|
||||
// 总体评价文案库
|
||||
static const List<String> overallTexts = [
|
||||
"老师讲课认真负责,课程内容充实丰富,理论与实践结合得很好,让我收获颇丰,对专业知识有了更深入的理解",
|
||||
"老师授课条理清晰,课程设置合理,由浅入深,循序渐进,学习过程中既有挑战性又能跟上节奏",
|
||||
"老师教学方法灵活多样,课程内容非常实用,学到的知识能够应用到实际中,让我感受到了学以致用的乐趣",
|
||||
"老师讲课生动有趣,课程内容丰富多彩,涵盖面广,开阔了我的视野,激发了我对这个领域更浓厚的兴趣",
|
||||
"老师治学严谨,循循善诱,通过这门课程让我建立了完整的知识体系,培养了逻辑思维能力和分析问题的能力",
|
||||
"老师授课重点突出,课程难度适中,既巩固了基础知识,又拓展了深度内容,满足了我的学习需求",
|
||||
"老师善于启发学生思考,课程注重培养实践能力和创新思维,让我不仅学到了知识,更学会了如何解决问题",
|
||||
"老师讲解详细透彻,课程安排紧凑合理,通过学习让我对该学科有了系统而全面的认识",
|
||||
"老师课堂气氛活跃,能调动学生积极性,这门课程很有启发性,培养了我的自主学习能力和探索精神",
|
||||
"老师教学认真,内容讲授清晰明确,课程与时俱进,紧跟学科发展,整体学习体验非常好,让我受益匪浅",
|
||||
"老师备课充分,课程内容环环相扣,逻辑严密,让我掌握了扎实的专业基础知识",
|
||||
"老师授课富有激情,课程设计新颖独特,学习过程充满乐趣,让我对学习保持了浓厚兴趣",
|
||||
"老师对学生认真负责,课程作业设置合理,既能巩固知识又不会过于繁重,学习效果很好",
|
||||
"老师讲课深入浅出,课程内容由易到难,知识点讲解透彻,让我能够循序渐进地掌握知识",
|
||||
"老师善于互动交流,课程注重学生参与,让我在积极的课堂氛围中提高了学习效率",
|
||||
"老师专业素养高,课程内容前沿实用,让我了解到了学科的最新发展动态和应用前景",
|
||||
"老师授课方式灵活,课程形式多样,既有理论讲解又有案例分析,让学习更加立体生动",
|
||||
"老师对学生耐心指导,课程考核方式合理,既注重过程又关注结果,让我全面提升了能力",
|
||||
"老师治学态度严谨,课程内容系统完整,帮助我构建了完善的知识框架和学科思维",
|
||||
"老师善于举例说明,课程理论联系实际,让抽象的概念变得具体易懂,提高了我的理解能力",
|
||||
"老师课堂管理有序,课程进度把握得当,既保证了教学质量又照顾到了学生的接受能力",
|
||||
"老师讲课条理分明,课程重难点突出,让我能够抓住学习的关键,提高了学习效率",
|
||||
"老师对学生要求严格,课程训练扎实有效,让我养成了良好的学习习惯和严谨的学习态度",
|
||||
"老师授课语言生动,课程内容引人入胜,每节课都能让我保持高度的专注和学习热情",
|
||||
"老师善于总结归纳,课程知识点梳理清晰,帮助我建立了清晰的知识脉络和记忆框架",
|
||||
"老师注重能力培养,课程不仅传授知识更注重方法,让我掌握了学习和研究的基本方法",
|
||||
"老师课堂效果显著,课程学习收获很大,不仅提升了专业水平也拓宽了思维视野",
|
||||
"老师教学经验丰富,课程设计科学合理,让我在轻松愉快的氛围中完成了学习任务",
|
||||
"老师对学生关怀备至,课程教学以学生为中心,充分考虑了我们的实际需求和接受能力",
|
||||
"老师讲课精彩纷呈,课程内容充实饱满,每次上课都有新的收获和感悟,让我的学习充满期待",
|
||||
];
|
||||
|
||||
/// Generate text based on question type
|
||||
/// Returns a random text from the appropriate text library
|
||||
String generate(QuestionType type) {
|
||||
String text;
|
||||
|
||||
switch (type) {
|
||||
case QuestionType.inspiration:
|
||||
text = inspirationTexts[_random.nextInt(inspirationTexts.length)];
|
||||
break;
|
||||
case QuestionType.suggestion:
|
||||
text = suggestionTexts[_random.nextInt(suggestionTexts.length)];
|
||||
break;
|
||||
case QuestionType.overall:
|
||||
text = overallTexts[_random.nextInt(overallTexts.length)];
|
||||
break;
|
||||
case QuestionType.general:
|
||||
// For general type, use overall texts as fallback
|
||||
text = overallTexts[_random.nextInt(overallTexts.length)];
|
||||
break;
|
||||
}
|
||||
|
||||
// Apply text processing rules
|
||||
text = _removeSpaces(text);
|
||||
text = _ensureMinLength(text);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/// Ensure text has at least 4 characters
|
||||
/// If text is shorter, pad with appropriate content
|
||||
String _ensureMinLength(String text) {
|
||||
if (text.length >= 4) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// For very short texts like "无" or "没有", they are already valid
|
||||
// Just return as is since Chinese characters count as valid
|
||||
return text;
|
||||
}
|
||||
|
||||
/// Remove all spaces from text
|
||||
String _removeSpaces(String text) {
|
||||
return text.replaceAll(' ', '');
|
||||
}
|
||||
|
||||
/// Validate if text meets all requirements
|
||||
/// Returns true if text is valid, false otherwise
|
||||
bool validate(String text) {
|
||||
// Check minimum length (at least 4 characters)
|
||||
if (text.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for spaces (should not contain any)
|
||||
if (text.contains(' ')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for 3 or more consecutive identical characters
|
||||
if (_hasConsecutiveChars(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Check if text has 3 or more consecutive identical characters
|
||||
/// Returns true if found, false otherwise
|
||||
bool _hasConsecutiveChars(String text) {
|
||||
if (text.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < text.length - 2; i++) {
|
||||
if (text[i] == text[i + 1] && text[i] == text[i + 2]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user