涉及功能: 文件夹上传、文件元数据管理、前后端数据一致性
一、问题描述
1.1 现象
上传文件夹时出现以下异常行为:
- 上传过程中,文件会临时出现在当前目录的文件列表中
- 文件名显示为带路径的形式,如
ddd/ddd/fdfs.mp3 - 刷新后文件消失,变成正常的文件夹结构
- 后续上传的文件仍然会出现在根目录,上传完成后刷新才正常
1.2 我更希望
- 上传文件夹时,文件应该直接显示在正确的子目录位置
- 或者不显示临时状态,等待上传完成后统一展示
- 存储本身是正确的(刷新后显示正常)
二、问题分析
2.1 数据流追踪
┌─────────────────────────────────────────────────────────────────┐│ 前端 (Flutter) │├─────────────────────────────────────────────────────────────────┤│ 1. _uploadFolder() 选择文件夹 ││ 2. path.relative() 计算相对路径 → Windows: "folder\file.txt" ││ 3. replaceAll('\\', '/') → "folder/file.txt" ││ 4. enqueueUpload(fileName: "myFolder/folder/file.txt") │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ 后端 (Go) │├─────────────────────────────────────────────────────────────────┤│ 5. sanitizeFilePath() 清理路径 ││ 6. 解析 fileName → actualFileName + actualRemotePath ││ "myFolder/folder/file.txt" → name:"file.txt", path:"/myFolder/folder/"│ 7. ensureParentFolders() 自动创建中间目录 ││ 8. 保存 FileMetadata { name: "file.txt", path: "/myFolder/folder/" }│ 9. UploadProgress { fileId: "xxx", phase: "done" } │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ 前端 (Flutter) │├─────────────────────────────────────────────────────────────────┤│ 10. _handleUploadSuccess() 收到 fileId ││ 11. 【问题所在】自己构建 FileMetadata: ││ { name: "myFolder/folder/file.txt", path: "/" } ← 错误! ││ 12. _files.add(metadata) → 在根目录显示带路径的文件名 │└─────────────────────────────────────────────────────────────────┘2.2 根因定位
问题1:后端路径解析(已修复)
- Windows 上
path.relative()返回使用\的路径 - 后端
sanitizeFilePath()只处理/,无法识别\ sanitizePathSegment()将\替换为_,破坏路径结构
问题2:前端临时 Metadata 构建(核心问题)
- 前端在
_handleUploadSuccess()中自己构建FileMetadata - 使用原始
fileName(可能包含路径)作为name - 使用
job.remotePath(当前目录)作为path - 结果:文件显示在错误的位置,文件名带路径
三、方案演进
3.1 初始方案:补丁式修复
思路:在前端增加条件判断,如果 fileName 包含 /,则不添加临时 metadata。
// 补丁方案(已废弃)
void _handleUploadSuccess(...) {
// 如果 fileName 包含路径分隔符,不添加临时 metadata
if (!fileName.contains('/')) {
final metadata = FileMetadata(
id: fileId,
name: fileName,
path: job.remotePath,
// ...
);
_files.add(metadata);
}
// ...
}问题:
- 代码变得越来越复杂,充满条件判断
- 前端需要”猜测”后端的行为
- 违反了”好品味”原则 —— 通过增加条件来处理特殊情况
3.2 我这边用的时候发现
“难道没有什么更加统一的更加好的方法吗,这种缝缝补补会非常大程度降低代码维护和可读性,对于bug,我们在现在开发早期阶段应该首先从自身架构修复出发,而不是缝缝补补”
这个反馈完全正确。根据 Linus 的”好品味”原则:
- 好代码没有特殊情况
- 与其增加条件判断,不如重新设计数据流
3.3 最终方案:架构级修复
核心思想:后端是数据的权威来源,前端不应该”猜测”构建 metadata。
解决方案:后端上传完成时返回完整的 FileMetadata,前端直接使用。
修复前的数据流: 后端: UploadProgress { fileId: "xxx" } 前端: 自己构建 FileMetadata { name: ?, path: ? } ← 猜测,容易出错
修复后的数据流: 后端: UploadProgress { fileId: "xxx", metadata: FileMetadata } 前端: 直接使用 progress.metadata ← 权威数据,零猜测四、实现细节
4.1 后端修改
types.go - 添加 Metadata 字段
// 修改前
type UploadProgress struct {
TaskID string `json:"taskId"`
Phase string `json:"phase"`
// ...
FileID string `json:"fileId,omitempty"`
Cancelled bool `json:"-"`
}
// 修改后
type UploadProgress struct {
TaskID string `json:"taskId"`
Phase string `json:"phase"`
// ...
FileID string `json:"fileId,omitempty"`
Metadata *FileMetadata `json:"metadata,omitempty"` // 新增:完整元数据
Cancelled bool `json:"-"`
}files.go - 上传完成时返回 Metadata
// 修改前
if progress != nil {
progress.Phase = "done"
progress.Progress = 100
progress.FileID = fileID
}
// 修改后
if progress != nil {
progress.Phase = "done"
progress.Progress = 100
progress.FileID = fileID
progress.Metadata = &metadata // 新增:返回完整元数据
}关键点:metadata 变量在后端已经正确构建,包含:
Name: “file.txt”(不含路径)Path: “/myFolder/folder/“(正确的远程路径)
4.2 前端修改
upload_progress.dart - 解析 Metadata
// 修改前
class UploadProgress {
final String? fileId;
// ...
}
// 修改后
import 'file_metadata.dart';
class UploadProgress {
final String? fileId;
final FileMetadata? metadata; // 新增
// ...
factory UploadProgress.fromJson(Map<String, dynamic> json) {
return UploadProgress(
// ...
fileId: json['fileId'],
metadata: json['metadata'] != null
? FileMetadata.fromJson(Map<String, dynamic>.from(json['metadata']))
: null,
);
}
}app_state.dart - 使用后端返回的 Metadata
// 修改前:前端自己构建 metadata
void _handleUploadSuccess(_UploadJob job, String fileId, String fileName, int fileSize) {
if (!fileName.contains('/')) { // 补丁式判断
final metadata = FileMetadata(
id: fileId,
name: fileName, // 可能错误
path: job.remotePath, // 可能错误
// ...
);
_files.add(metadata);
}
// ...
}
// 修改后:直接使用后端返回的 metadata
void _handleUploadSuccess(_UploadJob job, UploadProgress progress) {
// 后端返回了完整的 metadata,直接使用
if (progress.metadata != null) {
_files.add(progress.metadata!); // 权威数据,零条件判断
}
// ...
}4.3 后端路径处理修复(附带)
utils.go - 支持 Windows 反斜杠
// 修改前
func sanitizeFilePath(filePath string) string {
parts := strings.Split(filePath, "/") // 只处理 /
// ...
}
// 修改后
func sanitizeFilePath(filePath string) string {
// 先将 Windows 的反斜杠统一转换为正斜杠
filePath = strings.ReplaceAll(filePath, "\\", "/")
parts := strings.Split(filePath, "/")
// ...
}五、方案对比
| 维度 | 补丁方案 | 架构方案 |
|---|---|---|
| 复杂度 | 高(增加条件判断) | 低(删除条件判断) |
| 数据一致性 | 弱(前端猜测) | 强(后端权威) |
| 可维护性 | 差(分散逻辑) | 好(单一数据源) |
| 扩展性 | 差(新增特殊情况需新增判断) | 好(后端统一处理) |
| 代码行数 | +15 行条件判断 | -18 行(删除猜测逻辑) |
六、设计原则回顾
6.1 “好品味”原则应用
“有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。”
补丁方案的问题:
// 充满特殊情况的代码
if (!fileName.contains('/')) {
// 正常情况
_files.add(metadata);
}
// 包含路径的文件(特殊情况)不添加架构方案的优雅:
// 没有特殊情况,统一处理
if (progress.metadata != null) {
_files.add(progress.metadata!);
}6.2 数据流设计原则
Bad: 前端猜测后端行为
后端: { fileId: "xxx" }前端: 我来猜猜这个文件的 name 和 path 是什么...Good: 后端是权威数据源
后端: { fileId: "xxx", metadata: { name: "file.txt", path: "/folder/" } }前端: 直接使用,不需要猜测6.3 实用主义原则
“Theory and practice sometimes clash. Theory loses. Every single time.”
不是为了”架构正确”而重构,而是因为:
- 补丁方案引入了更多复杂性
- 前端猜测逻辑容易出错
- 后端已经有正确的数据,只是没有返回
架构方案实际上是更简单的方案。
七、文件修改清单
| 文件 | 修改类型 | 修改内容 |
|---|---|---|
core/internal/api/types.go | 结构体 | UploadProgress 添加 Metadata *FileMetadata 字段 |
core/internal/api/files.go | 逻辑 | 上传完成时 progress.Metadata = &metadata |
core/internal/api/utils.go | 逻辑 | sanitizeFilePath 支持 Windows 反斜杠 |
client/lib/core/models/upload_progress.dart | 模型 | 添加 metadata 字段和解析逻辑 |
client/lib/core/state/app_state.dart | 逻辑 | _handleUploadSuccess 使用后端返回的 metadata |
八、验证
-
go vet ./...通过 -
flutter analyze通过(仅有预存在的 info 级别警告) - 功能测试:上传文件夹,观察文件列表实时更新
九、经验总结
- 早期阶段优先架构修复:补丁式修复只会让代码越来越复杂
- 数据流分析是关键:画出完整的数据流向,问题自然暴露
- 后端是权威来源:前端不应该猜测后端行为,应该使用后端返回的权威数据
- 好代码没有特殊情况:如果需要增加条件判断来处理特殊情况,说明数据结构设计有问题
- 简单方案往往是最好的方案:架构方案看似”大动作”,实际上删除了更多代码
十、后续可能的优化
- 上传文件夹时的进度显示:考虑添加文件夹级别的进度(已上传 3/10 个文件)
- 批量 metadata 更新:上传文件夹时,考虑一次性获取所有新增文件的 metadata,减少 UI 刷新次数
- 乐观更新策略:对于简单文件(非文件夹),可以保留前端乐观更新;对于复杂路径,依赖后端返回