一、背景
1.1 原有问题
-
错误信息不友好:
- 后端返回的错误直接直接显示(如 “S3 connection failed”)
- 没有中文本地化
- 缺少错误分类和具体建议
-
传输失败无法重试:
- 上传/下载失败后只能删除重新添加
- 大文件重传浪费时间和流量
-
健康检查信息不直观:
- 调试页面只显示原始 JSON
- 不太好理解当前状态
二、错误处理增强
2.1 API 错误解析改进
api_client.dart
class ApiClient {
/// 统一错误处理
ApiResult<T> _handleError<T>(dynamic error) {
String message = '未知错误';
if (error is DioException) {
if (error.response != null) {
// 尝试解析后端返回的错误信息
final data = error.response?.data;
if (data is Map && data.containsKey('error')) {
message = _localizeError(data['error']);
} else if (error.response?.statusCode == 401) {
message = '认证失败,请重新解锁';
} else if (error.response?.statusCode == 503) {
message = 'S3 连接失败,请检查配置';
} else {
message = '服务器错误 (${error.response?.statusCode})';
}
} else if (error.type == DioExceptionType.connectionTimeout) {
message = '连接超时,请检查网络';
} else if (error.type == DioExceptionType.receiveTimeout) {
message = '响应超时,请重试';
} else if (error.type == DioExceptionType.connectionError) {
message = '无法连接到服务器';
} else {
message = '网络请求失败';
}
}
return ApiResult.error(message);
}
/// 错误本地化
String _localizeError(String error) {
// 常见错误映射
final errorMap = {
'vault not initialized': '加密库未初始化',
'vault not unlocked': '加密库未解锁',
'vault already initialized': '加密库已初始化',
'incorrect password': '密码错误',
'file not found': '文件不存在',
'file already exists': '文件已存在',
'invalid file name': '文件名无效',
'system folder cannot be modified': '系统文件夹无法修改',
'S3 connection failed': 'S3 连接失败',
'S3_UNAVAILABLE': 'S3 服务不可用',
};
// 尝试完全匹配
if (errorMap.containsKey(error)) {
return errorMap[error]!;
}
// 尝试部分匹配
for (final entry in errorMap.entries) {
if (error.toLowerCase().contains(entry.key.toLowerCase())) {
return entry.value;
}
}
// 返回原始错误(添加前缀)
return '错误: $error';
}
}改进点:
- 识别 HTTP 状态码并给出对应提示
- 识别网络异常类型(超时、连接失败等)
- 后端错误信息本地化
- 部分匹配机制处理未预见的错误
2.2 AppState 错误传播
class AppState {
/// 删除文件(增强错误处理)
Future<void> deleteFile(String id) async {
final result = await api.deleteFile(id);
if (result.isError) {
// 错误已经在 ApiClient 中本地化
showAppToast(context, result.error!);
return;
}
// 成功:更新本地状态
_files.removeWhere((f) => f.id == id);
await db.deleteFile(id: id, s3ConfigId: _activeS3ConfigId!);
await refreshFiles();
showAppToast(context, '删除成功');
}
/// 上传文件(增强错误处理)
Future<void> uploadFile(String filePath, String remotePath) async {
try {
final result = await api.uploadFile(
filePath: filePath,
remotePath: remotePath,
);
if (result.isSuccess) {
showAppToast(context, '上传成功');
} else {
// 具体的错误信息
showAppToast(context, '上传失败: ${result.error}');
}
} catch (e) {
// 未捕获的异常
showAppToast(context, '上传异常: $e');
}
}
}三、传输任务重试功能
3.1 重试接口设计
class AppState {
/// 重试失败的上传任务
Future<void> retryUpload(String taskId) async {
// 1. 从数据库加载任务详情
final task = await db.getTransferById(taskId);
if (task == null) {
showAppToast(context, '任务不存在');
return;
}
// 2. 检查原始文件是否还存在
final file = io.File(task.filePath);
if (!await file.exists()) {
showAppToast(context, '原始文件不存在,无法重试');
return;
}
// 3. 更新任务状态为排队
await db.updateTransfer(
id: taskId,
status: TransferStatus.queued.index,
error: null,
);
// 4. 重新加入上传队列
final job = _UploadJob(
id: taskId,
filePath: task.filePath,
fileName: task.name,
remotePath: task.remotePath,
);
_uploadQueue.add(job);
_uploadJobs[taskId] = job;
// 5. 启动上传处理
_processUploadQueue();
showAppToast(context, '已重新加入上传队列');
}
/// 重试失败的下载任务
Future<void> retryDownload(String taskId) async {
final task = await db.getTransferById(taskId);
if (task == null) {
showAppToast(context, '任务不存在');
return;
}
await db.updateTransfer(
id: taskId,
status: TransferStatus.queued.index,
error: null,
);
final job = _DownloadJob(
id: taskId,
file: task.fileMetadata,
localRelativeDir: task.localDir,
);
_downloadQueue.add(job);
_downloadJobs[taskId] = job;
_processDownloadQueue();
showAppToast(context, '已重新加入下载队列');
}
}3.2 传输页面重试按钮
// transfers_page.dart
Widget _buildTransferItem(TransferItem item) {
return ListTile(
leading: _buildStatusIcon(item.status),
title: Text(item.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.size != null)
Text(_formatSize(item.size!)),
if (item.status == TransferStatus.running)
LinearProgressIndicator(value: item.progress),
if (item.error != null)
Text(
item.error!,
style: TextStyle(color: Colors.red, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
trailing: _buildActions(item),
);
}
Widget _buildActions(TransferItem item) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// 失败任务显示重试按钮
if (item.status == TransferStatus.failed)
IconButton(
icon: Icon(Icons.refresh, size: 20),
onPressed: () => _retryTransfer(item),
tooltip: '重试',
),
// 运行中任务显示取消按钮
if (item.status == TransferStatus.running ||
item.status == TransferStatus.queued)
IconButton(
icon: Icon(Icons.close, size: 20),
onPressed: () => _cancelTransfer(item),
tooltip: '取消',
),
],
);
}
Future<void> _retryTransfer(TransferItem item) async {
if (item.type == TransferType.upload) {
await state.retryUpload(item.id);
} else {
await state.retryDownload(item.id);
}
}四、调试页面健康检查优化
4.1 结构化显示
// debug_page.dart
Future<void> _checkCoreStatus() async {
final sb = StringBuffer();
// 1. 核心模式
sb.writeln('━━━ 核心模式 ━━━');
sb.writeln('模式: ${state.coreMode}');
sb.writeln('地址: ${state.api.baseUrl}');
sb.writeln();
// 2. S3 配置
sb.writeln('━━━ S3 配置 ━━━');
if (state.hasS3Credentials) {
sb.writeln('✓ 配置名: ${state.activeS3ConfigName}');
sb.writeln('✓ 端点: ${state.s3Endpoint}');
sb.writeln('✓ 桶名: ${state.s3Bucket}');
} else {
sb.writeln('✗ 未配置 S3');
}
sb.writeln();
// 3. 健康检查
sb.writeln('━━━ 健康检查 ━━━');
final healthResult = await state.api.getHealthDetail();
if (healthResult != null) {
final status = healthResult['status'] ?? 'unknown';
sb.writeln('核心状态: $status');
if (status == 'ok') {
sb.writeln('✓ 核心运行正常');
} else {
final code = healthResult['code'] ?? '';
final error = healthResult['error'] ?? '';
sb.writeln('✗ 错误代码: $code');
sb.writeln('✗ 错误信息: $error');
// 提供建议
if (code == 'S3_UNAVAILABLE') {
sb.writeln();
sb.writeln('建议:');
sb.writeln('1. 检查 S3 服务是否运行');
sb.writeln('2. 检查网络连接');
sb.writeln('3. 检查 S3 配置是否正确');
}
}
} else {
sb.writeln('✗ 无法连接到核心');
sb.writeln();
sb.writeln('建议:');
sb.writeln('1. 检查核心是否启动');
sb.writeln('2. 检查端口是否正确');
sb.writeln('3. 尝试重启核心');
}
sb.writeln();
// 4. Bootstrap 状态
sb.writeln('━━━ 初始化状态 ━━━');
final bootstrapResult = await state.api.bootstrap();
if (bootstrapResult.isSuccess) {
final data = bootstrapResult.data!;
sb.writeln('状态: ${data.status}');
if (data.status == 'initialized') {
sb.writeln('✓ 加密库已初始化');
} else if (data.status == 'empty') {
sb.writeln('⚠ 桶为空,需要初始化');
} else if (data.status == 'conflict') {
sb.writeln('⚠ 桶中有文件但未初始化');
sb.writeln(' 对象数量: ${data.objectCount}');
}
} else {
sb.writeln('✗ 无法获取状态');
}
// 显示结果
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('核心状态'),
content: SingleChildScrollView(
child: Text(
sb.toString(),
style: TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('关闭'),
),
],
),
);
}改进点:
- 分区块显示(核心、S3、健康、初始化)
- 使用符号标识状态(✓ ✗ ⚠)
- 提供问题建议
- Monospace 字体,易于阅读
五、离线错误提示优化
5.1 文件页面空态改进
// files_page.dart
Widget _buildEmptyPlaceholder(AppState state) {
// 区分不同的离线原因
final s3Failed = state.isOffline &&
state.healthErrorCode == 'S3_UNAVAILABLE';
if (s3Failed) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.cloud_off, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'S3 连接失败',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'请检查存储服务配置后重试',
style: TextStyle(color: Colors.grey),
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
// 跳转到 S3 配置页面
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => S3ConfigPage(),
),
);
},
icon: Icon(Icons.settings),
label: Text('检查配置'),
),
],
);
}
if (state.isOffline) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'内核未启动',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'请启动内核后刷新',
style: TextStyle(color: Colors.grey),
),
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => state.refreshHealthStatus(),
icon: Icon(Icons.refresh),
label: Text('重试'),
),
],
);
}
// 在线但文件列表为空
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.folder_open, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('这里空空如也'),
SizedBox(height: 8),
Text(
'点击右下角按钮上传文件',
style: TextStyle(color: Colors.grey),
),
],
),
);
}改进点:
- 区分 S3 失败和内核未启动
- 提供具体的操作建议
- 直接跳转到相关设置页面
六、效果对比
6.1 错误提示对比
| 场景 | 旧版提示 | 新版提示 |
|---|---|---|
| S3 连接失败 | S3 connection failed | S3 连接失败,请检查配置 |
| 密码错误 | incorrect password | 密码错误 |
| 文件不存在 | file not found | 文件不存在 |
| 网络超时 | DioError [ConnectionTimeout] | 连接超时,请检查网络 |
| 系统文件夹 | system folder cannot be modified | 系统文件夹无法修改 |
6.2 传输管理对比
| 功能 | 旧版 | 新版 |
|---|---|---|
| 失败重试 | ❌ 只能删除重新上传 | ✅ 一键重试 |
| 错误信息 | ⚠️ 技术性错误 | ✅ 更顺手的提示 |
| 任务状态 | ⚠️ 简单文本 | ✅ 图标 + 进度条 + 错误详情 |
七、文件变更清单
client/lib/core/api/api_client.dart- 增强错误解析和本地化client/lib/core/state/app_state.dart- 添加重试功能client/lib/ui/files_page.dart- 优化错误提示client/lib/ui/transfers_page.dart- 添加重试按钮client/lib/ui/debug_page.dart- 优化健康检查显示
总结:通过错误本地化、传输重试、结构化显示等改进,显著提升了错误处理的使用体验和调试效率。