从【项目思维】→【产品思维】:代码封装

从【项目思维】→【产品思维】:代码封装(结合你工控 / 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);
}

问题

  1. 业务层强依赖 MinIO SDK;哪天换成 SeaweedFS,所有调用这个方法的地方都要改。
  2. Bucket、地址硬编码;不同产线不同桶,只能复制改代码。
  3. 没有统一返回结构、统一异常、日志;上传失败处理写死。
  4. 只能上传图片,后续加文件类型、加本地归档、加文件校验,代码越堆越乱。

二、产品思维封装思路(分层、依赖倒置、接口隔离)

核心原则:调用方只关心【要上传文件】,不关心底层是 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

四、产品思维额外工程能力(工业软件必备,项目一般不做)

  1. 重试机制:网络抖动上传失败,封装层内置重试,业务无感知(产线采集网络不稳非常关键)
  2. 文件校验:文件存在性、大小校验、后缀过滤,放在封装内部
  3. 兜底策略:对象存储故障,可自动降级本地磁盘缓存,后续异步续传(MES 抓拍很实用)
  4. 统一监控埋点:上传耗时、失败计数,埋点写在封装层,业务不用加埋点代码
  5. 版本兼容:接口契约固定,后续升级存储 SDK,不破坏上层业务调用

五、落地精简口诀(方便记)

  1. 抽接口:稳定契约,对外不动
  2. 分实现:每个存储单独写,内部细节隐藏
  3. 配置化:地址、桶名、超时全部放配置,禁止硬编码
  4. 工厂选择:一行配置切换存储,业务代码零改动
  5. 横切封装:日志、重试、异常统一放在底层,业务只写业务

六、你下一步可以直接落地

  1. 先定义IFileStorage接口 + FileUploadResult返回类(这就是你的组件契约)
  2. 先实现 MinIO 版本跑通业务
  3. 后续需要 SeaweedFS 时,新增SeaweedFileStorage,上层 MES 业务代码一行不用改

要不要我把这套完整代码加上重试、日志、文件校验,做成可以直接放进你的采集项目里的完整组件?

www.erwa.cn 二娃备忘

上一篇 MinIO ,RustFS ,SeaweedFS,Garage 测评一下能不能抗住 最能抗高并发,大图片上传,工厂车间稳定上传,断点续传,分片上传,500台设备上传,每天要存5T图片,硬盘,存60天。一般nas扛不住,群晖,飞牛fnOS,还有没有更好的方案
下一篇 🏅安装操作系统,磁盘分区:Linux(XFS , FAT32, ext4 )Windows(FAT32,NTFS)
signoz和日志系统 elk的区别?

signoz和日志系统 elk的区别?

SigNoz vs ELK(Elastic Stack),结合你 C# 上位机 + PLC 采集 的场景 一句话概括: ELK:以日志全文检索为核心的日志栈(ES 倒排索引,擅长全文搜报错文本),指标、链路追踪是附加能力,多组件拼凑 SigNoz:OTel 原生一体化可观测平台,

2026-10-20
0 0 0
DateTime.UtcNow , DateTime.Now 有什么区别

DateTime.UtcNow , DateTime.Now 有什么区别

DateTime.UtcNow vs DateTime.Now(C#) 一句话总结 DateTime.UtcNow:获取世界协调时间(UTC,零时区),不受电脑系统时区影响。 DateTime.Now:获取本地时间 = UTC 时间 + 当前操作系统时区偏移。 详

2026-10-16
0 0 0
winform 开机启动

winform 开机启动

c# winform 写的采集程序。平时是登录 windows 桌面,手动点开运行 winform 程序。但是要改成开机启动,不要登录 windows 桌面也能执行 winform 程序。有没有什么办法?不要改造 winform 程序 先说核心结论 WinForm

2026-10-16
0 0 0
C# + Scriban:生成 HTML 报表,自动发邮件(MES / 生产报表场景)

C# + Scriban:生成 HTML 报表,自动发邮件(MES / 生产报表场景)

补充:这套方案在工厂圈子使用情况 MailKit 自动发邮件:大量 MES 在用 Playwright 自动截图 HTML 报表:不少工厂自动化报表在用,属于成熟方案,但不是每个项目都会上。很多项目嫌麻烦就直接 PDF 或者纯表格邮件。 Scriban + Playwright

2026-10-14
0 0 0
C# + Scriban 做静态站点生成

C# + Scriban 做静态站点生成

C# + Scriban 做静态站点生成(对标 baifenwei 的 generate_site.py) Scriban 是 .NET 生态下高性能模板引擎,语法类似 Liquid / Jinja2,刚好用来替代 Python Jinja2,写一个 C# 控制台程序(GenerateSite

2026-10-14
0 0 0
REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向

REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向

REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向 全称 REA = Reverse Engineer Anything,“逆向一切” 只是项目口号,不是能力承诺。 https://rea.tools https://github.com/morluto

2026-10-14
0 0 0
采集上位机LOG文件,不停追加,文件大小50MB

采集上位机LOG文件,不停追加,文件大小50MB

txt 文件很大,40MB,50MB,txt 持续追加,这种场景。Logstash File Input、Flume Taildir、阿里云 Logtail、NewLifeX/X 日志查看工具 Klogg Log Parser Lizard https://githu

2026-10-10
0 0 0
AOI 图片上传 【分布式】方案

AOI 图片上传 【分布式】方案

车间 aoi 的相机图片上传,用飞牛 nas,群晖 nas,高并发,按 5TB / 天、60 天、500 台设备,能不能抗住?如果我在一台服务器放多个 ftp 服务,每条线一个 ftp 服务,端口 8001,第二条线 ftp 服务,端口 8002。ftp 靠谱还 nas 靠谱?稳定,生成环境

2026-10-09
0 0 0
MES 系统 的几种ID比较,雪花id,ULID,UUID

MES 系统 的几种ID比较,雪花id,ULID,UUID

实际场景:浏览器指纹和cookie的UUID 项目FingerprintJS visitorId bdc65ad12633d79792ceeca33a78e6c551.la 访客 ID 5a00c3da-ab80-513d-b7a7-894bcaaecf92来源读取浏览器硬件 / 软件特征,前

2026-10-09
0 0 0
ACCESS数据库访问报错

ACCESS数据库访问报错

AI最擅长找错,分析原因。 💥💥问题1:exclusively locked 因为ACCESS打开数据库后,表:TO_LOGGER_INFO ,当设计视图窗体时,另一个c#小程序去访问这个access数据库会报错: ERROR [HY000] [Microsoft][ODBC Mi

2026-09-29
0 0 0