从【项目思维】→【产品思维】:代码封装(结合你工控 / MES 采集、MinIO/SeaweedFS 上传场景举例)
一句话区分: ✅ 项目思维:为当前这一条产线、这一个需求写代码。能用就行,一次性交付,硬编码,业务逻辑和存储、接口混在一起。改一个地方到处动,换存储(MinIO 切 SeaweedFS)要大面积改代码。 ✅ 产品思维:代码是可复用组件,面向多客户、多产线、可配置、可扩展。把变化的部分和不变的部分隔离,对外提供稳定接口,内部随便换实现,调用方无感知。
产品化:代码版本控制,国际化(界面,提示,日志,手册多国语言),可配置,可扩展,只存win7(补丁),win10,ubuntu,x86,x64,arm芯片,浏览器兼容,找个商业软件参考
一、项目思维写法(反面例子,你大概率见过)
特点:耦合很重,直接在业务代码里写 MinIO 上传逻辑。
// 项目思维:业务代码直接写MinIO,写死bucket、路径
public async Task UploadPic(string imgName, string localPath)
{
var minioClient = new MinioClient("http://127.0.0.1:9000","ak","sk");
var args = new PutObjectArgs()
.WithBucket("factory-img")
.WithObject($"images/{imgName}")
.WithFileName(localPath);
await minioClient.PutObjectAsync(args);
}
问题
- 业务层强依赖 MinIO SDK;哪天换成 SeaweedFS,所有调用这个方法的地方都要改。
- Bucket、地址硬编码;不同产线不同桶,只能复制改代码。
- 没有统一返回结构、统一异常、日志;上传失败处理写死。
- 只能上传图片,后续加文件类型、加本地归档、加文件校验,代码越堆越乱。
二、产品思维封装思路(分层、依赖倒置、接口隔离)
核心原则:调用方只关心【要上传文件】,不关心底层是 MinIO / SeaweedFS / 本地磁盘
1. 先定义稳定抽象(接口,不变层,对外契约)
调用方永远只依赖这个接口,底层存储随便替换,上层代码一行不改。
/// <summary>
/// 文件存储统一接口【产品抽象,永不轻易改动】
/// </summary>
public interface IFileStorage
{
/// <summary>
/// 统一上传接口,对外只暴露业务名称+本地路径
/// </summary>
/// <param name="bizFileName">业务图片名,如line1_cam01.jpg</param>
/// <param name="localFilePath">本地文件全路径</param>
Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath);
Task<byte[]> DownloadAsync(string fileKey);
Task<bool> DeleteAsync(string fileKey);
}
/// <summary>
/// 统一返回实体,所有存储后端返回结构一致
/// </summary>
public class FileUploadResult
{
public string StorageType { get; set; } // MinIO / SeaweedFS
public string FileKey { get; set; } // 唯一key:minio对象名 / seaweed fileId
public string Url { get; set; }
public bool Success { get; set; }
public string Msg { get; set; }
}
2. 分别实现不同存储的具体实现(可变层)
每个存储单独一个实现类,各自的 SDK、路径规则放在内部
// MinIO实现
public class MinioFileStorage : IFileStorage
{
private readonly MinioConfig _config;
public MinioFileStorage(MinioConfig config)
{
_config = config; // 配置从配置文件读取,不硬编码
}
public async Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath)
{
// 内部拼接bucket、虚拟目录,调用方完全看不到
string objName = $"images/{bizFileName}";
// minio上传逻辑...
return new FileUploadResult
{
StorageType = "MinIO",
FileKey = objName,
Success = true
};
}
}
// SeaweedFS Filer实现
public class SeaweedFileStorage : IFileStorage
{
private readonly SeaweedConfig _config;
public SeaweedFileStorage(SeaweedConfig config)
{
_config = config;
}
public async Task<FileUploadResult> UploadAsync(string bizFileName, string localFilePath)
{
// http上传到Seaweed,拿到fileId
// 内部处理,调用方不用关心fileId机制
return new FileUploadResult
{
StorageType = "SeaweedFS",
FileKey = fileId,
Success = true
};
}
}
3. 工厂模式,根据配置自动选择存储(产品核心)
项目思维:写 if else 到处 new 客户端 产品思维:工厂统一创建实例,读配置切换存储,业务代码完全不用改
public class FileStorageFactory
{
public static IFileStorage GetStorage(StorageSetting setting)
{
return setting.StorageType switch
{
"MinIO" => new MinioFileStorage(setting.Minio),
"SeaweedFS" => new SeaweedFileStorage(setting.Seaweed),
_ => throw new NotSupportedException()
};
}
}
4. 业务层调用(最关键:业务代码极度干净)
业务层只依赖 IFileStorage 抽象,完全不知道底层是哪个存储
// MES产线业务代码,永远不变
var storage = FileStorageFactory.GetStorage(config);
var res = await storage.UploadAsync("line1_cam_20260928.jpg", @"D:\data\img.jpg");
if(res.Success)
{
//数据库保存res.FileKey
}
三、项目思维 vs 产品思维 封装对比(核心差异)
表格
| 维度 | 项目思维 | 产品思维 |
|---|---|---|
| 依赖 | 业务直接依赖 SDK,强耦合 | 业务依赖抽象接口,底层实现解耦 |
| 配置 | 硬编码,改代码才能换地址 /bucket | 全部抽离配置文件,不用改代码,重启生效 |
| 扩展 | 新增存储,业务代码全改 | 新增存储只新增一个实现类,上层不动(开闭原则) |
| 异常 / 日志 | 各处零散 try-catch | 统一在封装层处理日志、重试、异常,业务干净 |
| 交付目标 | 满足当前产线交付 | 一套组件,卖给多个客户,客户可自选存储方案 |
| 测试 | 只能整体联调 | 可以 Mock IFileStorage,单元测试,不用启动 MinIO |
四、产品思维额外工程能力(工业软件必备,项目一般不做)
- 重试机制:网络抖动上传失败,封装层内置重试,业务无感知(产线采集网络不稳非常关键)
- 文件校验:文件存在性、大小校验、后缀过滤,放在封装内部
- 兜底策略:对象存储故障,可自动降级本地磁盘缓存,后续异步续传(MES 抓拍很实用)
- 统一监控埋点:上传耗时、失败计数,埋点写在封装层,业务不用加埋点代码
- 版本兼容:接口契约固定,后续升级存储 SDK,不破坏上层业务调用
五、落地精简口诀(方便记)
- 抽接口:稳定契约,对外不动
- 分实现:每个存储单独写,内部细节隐藏
- 配置化:地址、桶名、超时全部放配置,禁止硬编码
- 工厂选择:一行配置切换存储,业务代码零改动
- 横切封装:日志、重试、异常统一放在底层,业务只写业务
六、你下一步可以直接落地
- 先定义
IFileStorage接口 +FileUploadResult返回类(这就是你的组件契约) - 先实现 MinIO 版本跑通业务
- 后续需要 SeaweedFS 时,新增
SeaweedFileStorage,上层 MES 业务代码一行不用改
要不要我把这套完整代码加上重试、日志、文件校验,做成可以直接放进你的采集项目里的完整组件?
www.erwa.cn 二娃备忘