基于设计模式的Winform软件框架-01Xml\Log\Ini日志(单例模式+生产者消费者模式)

目录

  1. 概述
  2. 架构设计
  3. 核心设计模式原理
  4. 核心概念
  5. 详细API文档
  6. 集成指南
  7. 常见问题FAQ
  8. 附录

1. 概述

Related_Log 是一个专为 Windows 桌面应用程序设计的日志和配置管理类库。它提供三大核心功能:

功能模块 说明 典型用途
INI配置管理 读写 INI 格式配置文件 保存设备参数、通讯配置、用户设置
文本日志系统 记录系统运行时的调试、警告、错误信息 问题诊断、系统监控、审计追踪
XML操作日志 记录用户操作行为(谁在什么时候做了什么) 安全审计、操作追溯、责任追溯

1.2 核心特性

  • 高性能异步写入:日志写入采用队列批处理机制,不阻塞主线程
  • 零依赖:仅依赖 .NET Framework 基础类库
  • 线程安全:所有服务均支持多线程并发调用
  • 自动分类存储:日志按级别和日期自动分目录、分文件存储
  • 易于集成:单例模式设计,一行代码即可调用

1.3 文件目录结构

复制代码
应用程序根目录/
├── config.ini              # INI配置文件(手动或通过API创建)
├── logs/                   # 文本日志目录(自动创建)
│   ├── INFO/              # 信息级别日志
│   │   ├── 2026-06-10.log # 按日期分文件
│   │   └── 2026-06-11.log
│   ├── WARN/              # 警告级别日志
│   ├── ERROR/             # 错误级别日志
│   ├── DEBUG/             # 调试级别日志
│   └── FATAL/             # 致命错误级别日志
└── logs_xml/              # XML操作日志目录(自动创建)
    ├── 2026-06-10_log.xml # 按日期分文件
    └── 2026-06-11_log.xml

2. 架构设计

2.1 设计模式

本类库综合运用了多种设计模式:

设计模式 应用位置 作用
单例模式 SystemConfigService 确保全局唯一访问入口
外观模式 SystemConfigService 隐藏内部复杂性,提供简洁API
生产者-消费者模式 LogQueueProcessor 异步队列处理,提升性能
观察者模式 LogMessage/XmlMessage 事件 实时推送日志到UI
策略模式 日志保留策略 灵活配置清理规则

2.2 类图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    SystemConfigService (单例)                    │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐ │
│  │   Config    │  │     Log     │  │          Xml             │ │
│  │ (属性公开)  │  │  (属性公开)  │  │      (属性公开)          │ │
│  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────────┘ │
└─────────┼─────────────────┼────────────────────┼────────────────┘
          │                 │                    │
          ▼                 ▼                    ▼
┌─────────────────┐ ┌─────────────┐   ┌─────────────────────┐
│IniConfigService│ │LogService   │   │XmlLogServiceWrapper │
│   (内部实现)    │ │  (封装类)   │   │    (封装类)          │
└─────────────────┘ └──────┬──────┘   └──────────┬──────────┘
                           │                    │
                           ▼                    ▼
                ┌─────────────────────────────────┐
                │       LogQueueProcessor          │
                │    (队列处理器 - 后台线程)        │
                │  ┌─────────────────────────┐    │
                │  │  _logQueue (文本日志队列) │    │
                │  │  _xmlQueue (XML日志队列) │    │
                │  └─────────────────────────┘    │
                └──────────────┬──────────────────┘
                               │
                ┌──────────────┴──────────────────┐
                ▼                                  ▼
        ┌───────────────┐              ┌──────────────┐
        │LogFileService │              │XmlLogService │
        │ (磁盘写入)    │              │ (磁盘写入)   │
        └───────────────┘              └──────────────┘

2.3 数据流程图

复制代码
┌──────────┐
│  用户代码  │
└─────┬────┘
      │ 调用API
      ▼
┌─────────────────────────────────────┐
│      SystemConfigService.Instance    │
│  .Log.Info("消息", "来源")            │
│  .Xml.Log(sender, "用户名")         │
│  .Config.Read("Section", "Key")      │
└─────────────────────────────────────┘
      │
      ├─→ INI读写:立即返回
      │
      ├─→ Log/Xml写入:
      │    1. 创建请求对象
      │    2. 放入内存队列(瞬间完成)
      │    3. 触发事件(UI实时显示)
      │    4. 后台线程批量写入文件
      │
      ▼
┌─────────────────────────────────────┐
│         磁盘文件                      │
│  config.ini / logs/ / logs_xml/    │
└─────────────────────────────────────┘

3. 核心设计模式原理

本章目标 :深入理解类库使用的两大核心设计模式------单例模式生产者-消费者模式(消息队列),帮助开发者掌握其原理并能够应用到自己的项目中。

3.1 单例模式(Singleton Pattern)原理详解

3.1.1 什么是单例模式?

单例模式 是一种创建型设计模式,它确保一个类在整个应用程序生命周期中只能有一个实例,并提供一个全局访问点来获取该实例。

生活中的类比

  • 打印机管理器:办公室里可能有多台电脑,但只有一台打印机。所有电脑通过同一个"打印机管理器"来排队打印任务。
  • Windows任务管理器:无论你打开多少次任务管理器,系统只允许一个窗口存在。
3.1.2 为什么需要单例模式?

在日志系统中使用单例模式的理由:

理由 说明
资源共享 日志文件是共享资源,多个实例同时写入会导致文件冲突
状态一致性 配置信息需要在整个程序中保持一致
性能优化 避免重复创建对象,节省内存
全局访问 任何地方都能方便地调用,无需传递引用
3.1.3 单例模式的实现方式
方式1:简单单例(非线程安全)
csharp 复制代码
public class SimpleSingleton
{
    private static SimpleSingleton _instance;  // 静态变量存储唯一实例

    private SimpleSingleton()  // 私有构造函数,防止外部创建实例
    {
    }

    public static SimpleSingleton Instance  // 全局访问点
    {
        get
        {
            if (_instance == null)  // 如果实例不存在,创建它
                _instance = new SimpleSingleton();
            return _instance;
        }
    }
}

问题 :多线程环境下,两个线程可能同时通过 if (_instance == null) 检查,导致创建多个实例!

方式2:线程安全的单例(双重检查锁定)
csharp 复制代码
public class ThreadSafeSingleton
{
    private static ThreadSafeSingleton _instance;
    private static readonly object _lock = new object();  // 锁对象

    private ThreadSafeSingleton()
    {
    }

    public static ThreadSafeSingleton Instance
    {
        get
        {
            if (_instance == null)  // 第一次检查(未加锁,提高性能)
            {
                lock (_lock)  // 加锁
                {
                    if (_instance == null)  // 第二次检查(加锁后)
                    {
                        _instance = new ThreadSafeSingleton();
                    }
                }
            }
            return _instance;
        }
    }
}

工作原理

  1. 第一次检查:如果实例已存在,直接返回,避免不必要的锁等待
  2. 加锁:确保同一时间只有一个线程能进入创建代码
  3. 第二次检查:防止多个线程同时通过第一次检查后,在锁内重复创建

流程图

复制代码
多个线程同时调用 Instance
         │
         ▼
    _instance == null?  ←───┐
         │                 │
    是/Yes              否/No │ 直接返回已存在的实例
         │                 │
         ▼                 │
    获取锁 _lock           │
         │                 │
         ▼                 │
    等待获取锁              │
         │                 │
         ▼                 │
    获得锁后再次检查 ───────┘
         │
    _instance == null?
         │
    是/Yes → 创建新实例 → 释放锁 → 返回实例
    否/No  → 释放锁 → 返回现有实例
方式3:本类库使用的写法(简化双重检查)
csharp 复制代码
public partial class SystemConfigService : IDisposable
{
    private static SystemConfigService _instance;
    private static readonly object _lock = new object();

    private SystemConfigService()  // 私有构造函数
    {
        // 初始化各个服务
        Config = new IniConfigService();
        // ...
    }

    public static SystemConfigService Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    // 使用 ?? 运算符简化写法
                    _instance = _instance ?? new SystemConfigService();
                }
            }
            return _instance;
        }
    }
}

代码解读

  • _instance = _instance ?? new SystemConfigService() 等价于:

    csharp 复制代码
    if (_instance == null)
        _instance = new SystemConfigService();
3.1.4 单例模式在类库中的应用
csharp 复制代码
// ========== 任何地方都可以这样调用 ==========
// 不需要 new,不需要传递引用
SystemConfigService.Instance.Log.Info("消息", "来源");

// ========== 在不同类中使用 ==========
public class FrmMain
{
    public void LoadData()
    {
        // 直接使用,无需传递
        SystemConfigService.Instance.Log.Info("加载开始", "FrmMain");
    }
}

public class DataService
{
    public void SaveData()
    {
        // 同一个实例,共享状态
        SystemConfigService.Instance.Log.Info("保存开始", "DataService");
    }
}
3.1.5 单例模式的优缺点
优点 缺点
✅ 全局唯一访问点 ❌ 难以单元测试(全局状态)
✅ 节省内存资源 ❌ 违反单一职责原则(管理自身+业务逻辑)
✅ 延迟初始化(用到时才创建) ❌ 隐藏依赖关系(类之间的耦合不明显)
✅ 线程安全(正确实现时) ❌ 可能被滥用(全局变量替代品)
3.1.6 何时使用单例模式?

适用场景

  • ✅ 日志服务、配置管理器、数据库连接池
  • ✅ 线程池、缓存管理器、打印机后台处理程序

不适用场景

  • ❌ 普通的业务对象(如用户、订单)
  • ❌ 需要多实例的对象(如网络连接)

3.2 生产者-消费者模式(消息队列)原理详解

3.2.1 什么是生产者-消费者模式?

生产者-消费者模式是一种并发设计模式,它解耦了"生产数据"和"处理数据"两个过程,通过一个**缓冲区(队列)**来连接它们。

生活中的类比

  • 餐厅后厨
    • 生产者:厨师(不断炒菜)
    • 队列:出餐口(放菜的地方)
    • 消费者:服务员(取菜送给客人)
  • 快递中转站
    • 生产者:快递员(不断送来包裹)
    • 队列:分拣传送带
    • 消费者:派送员(取走包裹派送)
3.2.2 为什么需要消息队列?

场景对比

对比项 直接写入磁盘(传统方式) 消息队列(本类库方式)
写入方式 每次调用立即写文件 先放入队列,后台批量写
主线程阻塞 每次写入可能耗时 5-50ms 入队耗时 <0.1ms
100条日志总耗时 500-5000ms 入队 <10ms,后台异步处理
用户体验 界面可能卡顿 界面流畅无感
磁盘I/O次数 100次(每条1次) 1-2次(批量)

核心优势

  1. 解耦:生产者和消费者互不依赖,可以独立变化
  2. 削峰:处理突发大量请求时,队列起到缓冲作用
  3. 异步:主线程快速返回,耗时操作在后台执行
  4. 批量处理:合并多次写入为一次,减少磁盘I/O
3.2.3 消息队列的工作原理
核心组件
复制代码
┌─────────────────────────────────────────────────────────────┐
│                      生产者-消费者模式                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────┐         ┌───────────┐         ┌──────────┐  │
│  │ 生产者    │  入队   │  消息队列  │  出队   │  消费者   │  │
│  │(多个线程)│ ───────→│ (缓冲区)  │ ───────→│(后台线程)│  │
│  └──────────┘         └───────────┘         └──────────┘  │
│       │                     │                    │          │
│       │                     │                    │          │
│   用户代码              内存队列              批量写入文件    │
│  调用Log.Info()      ConcurrentQueue       LogFileService  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
时序图
复制代码
时间线 ──────────────────────────────────────────────────────→

主线程(生产者):                       后台线程(消费者):
     │                                      │
     │ Log.Info("消息1")                    │
     ├─→ 创建LogRequest对象                 │
     ├─→ 入队(瞬间完成)                    │
     ├─→ 立即返回 <0.1ms ──────────────────→ 休眠中...
     │                                      │
     │ Log.Info("消息2")                    │
     ├─→ 创建LogRequest对象                 │
     ├─→ 入队                               │
     ├─→ 立即返回                           │
     │                                      │
     │ Log.Info("消息3")                    │
     ├─→ 创建LogRequest对象                 │
     │ ...(主线程继续运行)                 │
     │                                      │ 1秒后或队列满时被唤醒
     │                                      ├─→ 从队列取出100条
     │                                      ├─→ 批量写入文件(一次性)
     │                                      ├─→ 写入完成
     │                                      └─→ 继续休眠
3.2.4 本类库中的队列实现
队列处理器核心代码
csharp 复制代码
internal class LogQueueProcessor : IDisposable
{
    // 线程安全的队列(.NET 提供)
    private readonly ConcurrentQueue<LogRequest> _logQueue;
    private readonly ConcurrentQueue<XmlLogRequest> _xmlQueue;

    // 后台线程
    private Thread _logThread;
    private Thread _xmlThread;

    // 控制标志
    private bool _isRunning;
    private readonly ManualResetEventSlim _logEvent;  // 通知信号

    // 批量写入配置
    private const int BATCH_SIZE = 100;        // 每次批量写入100条
    private const int FLUSH_INTERVAL_MS = 1000; // 或者每1秒写一次

    public LogQueueProcessor(
        Action<LogRequest[]> processLogBatch,
        Action<XmlLogRequest[]> processXmlBatch)
    {
        _logQueue = new ConcurrentQueue<LogRequest>();
        _xmlQueue = new ConcurrentQueue<XmlLogRequest>();
        _logEvent = new ManualResetEventSlim(false);

        // 启动后台线程
        Start();
    }

    // ========== 生产者方法(主线程调用) ==========

    public void EnqueueLog(LogRequest request)
    {
        if (request == null) return;
        _logQueue.Enqueue(request);  // 入队(线程安全,<0.01ms)
        _logEvent.Set();  // 通知后台线程"有新数据"
    }

    // ========== 消费者方法(后台线程执行) ==========

    private void ProcessLogQueue()
    {
        while (_isRunning)
        {
            // 等待信号或超时(1秒)
            _logEvent.Wait(FLUSH_INTERVAL_MS);
            _logEvent.Reset();

            // 收集一批日志
            var batch = new LogRequest[BATCH_SIZE];
            int count = 0;

            // 从队列中取出(最多100条)
            while (count < BATCH_SIZE && _logQueue.TryDequeue(out var request))
            {
                batch[count++] = request;
            }

            // 如果有日志,批量处理
            if (count > 0)
            {
                Array.Resize(ref batch, count);  // 调整数组大小
                _processLogBatch(batch);  // 调用写入方法
            }
        }
    }
}
关键技术点解析

1. ConcurrentQueue - 线程安全队列

csharp 复制代码
// 普通队列(非线程安全)
Queue<LogRequest> queue = new Queue<LogRequest>();
queue.Enqueue(item);     // 多线程同时调用会出错
var item = queue.Dequeue();

// 线程安全队列
ConcurrentQueue<LogRequest> queue = new ConcurrentQueue<LogRequest>();
queue.Enqueue(item);     // 多线程同时调用安全 ✓
queue.TryDequeue(out item);  // 尝试取出,失败返回false

为什么需要线程安全?

  • 主线程(生产者)在入队
  • 后台线程(消费者)在出队
  • 两个线程同时操作同一个队列,必须使用线程安全的队列

2. ManualResetEventSlim - 线程间通信

csharp 复制代码
// 创建事件(初始状态:无信号)
var event = new ManualResetEventSlim(false);

// 后台线程:等待信号
event.Wait(1000);  // 等待1000ms或收到信号
event.Reset();     // 重置为无信号状态

// 主线程:发送信号
event.Set();  // 通知后台线程"醒来工作"

工作原理

复制代码
后台线程              事件状态              主线程
   │                   │                    │
   │ Wait()           □ 无信号              │
   │ (阻塞)            │                   │
   │                   │                   │ Enqueue()
   │                   │                   │
   │                   │                   ├─ Set()
   │                   │                   │
   │ ← 醒来!         ■ 有信号              │
   │ 开始工作          │                   │
   │                   │                   │
   │ Reset()          □ 无信号              │
   │ Wait()           │                   │
   │ (阻塞)            │                   │

3. 批量处理 - 性能优化的关键

csharp 复制代码
// ❌ 效率低:每次写一条
foreach (var log in logs)
{
    File.AppendAllText("file.log", log.ToString());  // 100次磁盘I/O
}

// ✅ 效率高:批量写入
var content = string.Join("\r\n", logs);
File.AppendAllText("file.log", content);  // 1次磁盘I/O

性能对比(100条日志):

  • 逐条写入:~500ms(100次磁盘I/O)
  • 批量写入:~50ms(1次磁盘I/O)
3.2.5 完整数据流程图
复制代码
┌──────────────────────────────────────────────────────────────────┐
│                         用户代码                                  │
│  SystemConfigService.Instance.Log.Info("设备连接成功", "PLC");  │
└────────────────────────────┬─────────────────────────────────────┘
                             │ 调用
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                        LogService.Log()                          │
│  1. 创建 LogRequest 对象(包含级别、消息、来源、时间戳)          │
│  2. 调用 _queueProcessor.EnqueueLog(request)  ← 瞬间完成         │
└────────────────────────────┬─────────────────────────────────────┘
                             │ 入队
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                     ConcurrentQueue<LogRequest>                   │
│                      内存中的日志队列                               │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐              │
│  │Log1 │ │Log2 │ │Log3 │ │Log4 │ │Log5 │ │ ... │  ...          │
│  └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘              │
└────────────────────────────┬─────────────────────────────────────┘
                             │ 后台线程定时取出(100条或1秒)
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                     LogFileService.WriteBatch()                   │
│  1. 按日期和级别分组                                               │
│  2. 拼接成字符串(用 \r\n 连接)                                  │
│  3. File.AppendAllText() 一次性写入文件                           │
└────────────────────────────┬─────────────────────────────────────┘
                             │ 写入
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│                     磁盘日志文件                                   │
│  logs/INFO/2026-06-11.log                                        │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ 2026-06-11 14:30:25.1234|INFO|PLC|设备连接成功            │   │
│  │ 2026-06-11 14:30:26.5678|INFO|Modbus|读取寄存器            │   │
│  │ ...                                                         │   │
│  └──────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
3.2.6 队列的优缺点与权衡
优点 缺点 权衡方案
✅ 主线程不被阻塞 ❌ 程序崩溃可能丢失队列中的数据 程序退出时调用 Flush()
✅ 批量写入,减少I/O ❌ 增加内存消耗 限制队列最大长度
✅ 处理突发流量 ❌ 日志可能有延迟 设置合理的刷新间隔
✅ 线程安全 ❌ 调试难度增加 添加队列大小监控
3.2.7 何时使用消息队列?

适用场景

  • ✅ 日志记录
  • ✅ 消息发送(邮件、短信)
  • ✅ 文件上传
  • ✅ 数据库批量写入
  • ✅ 任何"生产快、消费慢"的场景

不适用场景

  • ❌ 需要立即确认的操作(如支付)
  • ❌ 数据量极小,直接处理更简单
  • ❌ 消息不能丢失且无法容忍延迟

3.3 设计模式组合运用

本类库巧妙地将单例模式生产者-消费者模式组合使用:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                   组合架构示意                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌───────────────────────────────────────────────────────┐    │
│   │          SystemConfigService (单例)                   │    │
│   │    确保全局只有一个入口,管理所有日志和配置服务          │    │
│   └─────────────────────┬─────────────────────────────────┘    │
│                         │                                       │
│          ┌──────────────┼──────────────┐                      │
│          ▼              ▼              ▼                      │
│   ┌────────────┐  ┌────────────┐  ┌────────────┐            │
│   │ Config服务  │  │ Log服务    │  │ Xml服务     │            │
│   │ (立即执行)  │  │ (入队返回)  │  │ (入队返回)  │            │
│   └────────────┘  └──────┬─────┘  └──────┬─────┘            │
│                          │                │                    │
│                          ▼                ▼                    │
│                   ┌────────────────────────────┐             │
│                   │    LogQueueProcessor        │             │
│                   │   (生产者-消费者模式)         │             │
│                   │   统一的队列处理中心          │             │
│                   └─────────────┬──────────────┘             │
│                                │                            │
│                    ┌───────────┴───────────┐                │
│                    ▼                       ▼                │
│            ┌─────────────┐         ┌─────────────┐         │
│            │LogFileService│         │XmlLogService│         │
│            │  消费者:批量写入  │         │  消费者:批量写入  │         │
│            └─────────────┘         └─────────────┘         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

组合的优势

  1. 单例提供统一入口,调用简单
  2. 队列提供异步处理,性能优秀
  3. 两者结合,既好用又高效

4. 核心概念

3.1 日志级别(LogLevel)

文本日志支持 5 个级别,从低到高:

级别 枚举值 说明 适用场景
信息 LogLevel.Info 正常运行信息 连接成功、数据更新、流程完成
警告 LogLevel.Warn 潜在问题 参数异常、重连尝试、接近阈值
错误 LogLevel.Error 错误但可恢复 通讯失败、文件读写错误
调试 LogLevel.Debug 调试信息 详细数据、中间状态
致命 LogLevel.Fatal 严重错误 程序无法继续运行

代码示例

csharp 复制代码
SystemConfigService.Instance.Log.Info("设备连接成功", "FrmMain");
SystemConfigService.Instance.Log.Warn("通讯延迟超过阈值", "CommService");
SystemConfigService.Instance.Log.Error("读取寄存器失败", ex, "ModbusRTU");
SystemConfigService.Instance.Log.Debug("发送数据: 01 03 00 00 00 0A", "CommService");
SystemConfigService.Instance.Log.Fatal("系统崩溃,无法启动", ex, "Program");

3.2 日志保留策略(LogRetentionDays)

用于控制日志文件的清理周期:

枚举值 天数 说明
Never -1 不清理,永久保留
Days7 7 保留 7 天
Days30 30 保留 30 天(默认)
Days60 60 保留 60 天
Days90 90 保留 90 天
Days180 180 保留 180 天

3.3 INI配置文件结构

INI 文件采用经典的"节-键-值"结构:

ini 复制代码
[Section1]
Key1=Value1
Key2=Value2

[Section2]
Key1=Value1
Key2=Value2

示例

ini 复制代码
[ModbusTCP]
IP=192.168.1.100
Port=502
SlaveId=1

[Storage]
Save_Log=30天
Save_Xml=30天

5. 详细API文档

4.1 SystemConfigService - 统一访问入口

这是类库的主入口,采用单例模式,通过 SystemConfigService.Instance 访问。

4.1.1 属性
属性 类型 说明
Config IniConfigService INI配置服务实例
Log LogService 文本日志服务实例
Xml XmlLogServiceWrapper XML操作日志服务实例
4.1.2 方法

日志清理

csharp 复制代码
// 方式1:使用整数天数
CleanOldLogs(int logRetentionDays = -1, int xmlRetentionDays = -1)

// 方式2:使用枚举(推荐)
CleanOldLogs(LogRetentionDays logRetention, LogRetentionDays xmlRetention)

统计信息

csharp 复制代码
// 获取队列大小(用于监控)
(int logQueueSize, int xmlQueueSize) GetQueueSize()

// 获取日志占用空间
long GetTotalLogSize()

// 获取日志文件数量
(int logCount, int xmlCount) GetLogFileCount()

资源释放

csharp 复制代码
void Dispose()
4.1.3 事件
事件 参数类型 说明
LogMessage EventHandler<LogEventArgs> 文本日志消息事件
XmlMessage EventHandler<XmlLogEventArgs> XML日志消息事件

事件订阅示例

csharp 复制代码
SystemConfigService.Instance.LogMessage += OnLogReceived;
SystemConfigService.Instance.XmlMessage += OnXmlLogReceived;

private void OnLogReceived(object sender, LogEventArgs e)
{
    // e.Level - 日志级别
    // e.Message - 日志消息
    // e.Source - 来源
    // e.Timestamp - 时间戳
}

private void OnXmlLogReceived(object sender, XmlLogEventArgs e)
{
    // e.Username - 用户名
    // e.Action - 操作
    // e.Details - 详情
    // e.Timestamp - 时间戳
}

4.2 LogService - 文本日志服务

通过 SystemConfigService.Instance.Log 访问。

4.2.1 方法

基本方法

csharp 复制代码
// 记录信息
void Info(string message, string source = "")

// 记录警告
void Warn(string message, string source = "")

// 记录错误(无异常)
void Error(string message, string source = "")

// 记录错误(带异常)
void Error(string message, Exception ex, string source = "")

// 记录调试信息
void Debug(string message, string source = "")

// 记录致命错误(无异常)
void Fatal(string message, string source = "")

// 记录致命错误(带异常)
void Fatal(string message, Exception ex, string source = "")

// 核心方法(直接指定级别)
void Log(LogLevel level, string message, string source = "", Exception ex = null)
4.2.2 使用示例
csharp 复制代码
// 简单记录
SystemConfigService.Instance.Log.Info("应用程序启动", "Program");

// 带来源记录
SystemConfigService.Instance.Log.Warn("配置文件不存在,使用默认值", "ConfigService");

// 记录异常
try
{
    File.Delete("somefile.txt");
}
catch (Exception ex)
{
    SystemConfigService.Instance.Log.Error("删除文件失败", ex, "FileService");
}

// 使用核心方法
SystemConfigService.Instance.Log.Log(
    LogLevel.Info,
    "用户登录成功",
    "LoginService"
);
4.2.3 日志文件格式

文本日志文件采用管道符分隔格式:

复制代码
时间戳|级别|来源|消息

示例

复制代码
2026-06-11 14:30:25.1234|INFO|Program|应用程序启动
2026-06-11 14:30:26.4567|WARN|ConfigService|配置文件不存在,使用默认值
2026-06-11 14:30:27.7890|ERROR|ModbusRTU|读取寄存器失败 | 异常: TimeoutException: 连接超时 | 堆栈: at ModbusRTU.Read()...

4.3 XmlLogServiceWrapper - XML操作日志服务

通过 SystemConfigService.Instance.Xml 访问。

4.3.1 方法
csharp 复制代码
// 自动提取控件信息记录(推荐)
void Log(object sender, string username, string additionalAction = null, string details = null)

// 直接指定参数记录
void Log(string username, string action, string details = "")
4.3.2 使用示例
csharp 复制代码
// 在按钮点击事件中使用(推荐)
private void btnSave_Click(object sender, EventArgs e)
{
    // 自动提取控件名称、文本、页面信息
    SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);

    // 或者附加额外信息
    SystemConfigService.Instance.Xml.Log(
        sender,
        Environment.UserName,
        additionalAction: "保存配方",
        details: "配方名称: Recipe001"
    );
}

// 直接记录
SystemConfigService.Instance.Xml.Log(
    "Admin",
    "UserLogin",
    "IP: 192.168.1.100"
);
4.3.3 自动提取的信息

当使用 Log(object sender, ...) 重载时,系统会自动提取:

信息 来源 示例
控件名称 Control.Name btnSave
控件文本 Control.Text 保存
控件标签 Control.Tag SaveRecipe
页面名称 查找父级 UserControl Page_Recipe_01List
页面标题 Page.Text 配方列表

生成的 Action 格式:控件名-控件文本(Tag信息)-附加操作-被按下

4.3.4 XML日志文件格式

XML日志文件使用 .NET XML 序列化,结构如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfXmlLogEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <XmlLogEntry>
    <Timestamp>2026-06-11 14:30:25.123456</Timestamp>
    <CurrentUser>Admin</CurrentUser>
    <Action>btnSave-保存(SaveRecipe)-被按下</Action>
    <Details>页面:Page_Recipe_01List(配方列表); 配方名称: Recipe001</Details>
  </XmlLogEntry>
  <XmlLogEntry>
    <Timestamp>2026-06-11 14:31:10.654321</Timestamp>
    <CurrentUser>Admin</CurrentUser>
    <Action>UserLogin</Action>
    <Details>IP: 192.168.1.100</Details>
  </XmlLogEntry>
</ArrayOfXmlLogEntry>

4.4 IniConfigService - INI配置服务

通过 SystemConfigService.Instance.Config 访问。

4.4.1 读取方法
csharp 复制代码
// 读取字符串
string Read(string section, string key, string defaultValue = "")

// 读取整数
int ReadInt(string section, string key, int defaultValue = 0)

// 读取长整数
long ReadLong(string section, string key, long defaultValue = 0)

// 读取布尔值
bool ReadBool(string section, string key, bool defaultValue = false)

// 读取浮点数
double ReadDouble(string section, string key, double defaultValue = 0.0)

// 读取单精度浮点数
float ReadFloat(string section, string key, float defaultValue = 0.0f)

// 读取整个Section的所有键值对
Dictionary<string, string> ReadSection(string section)

// 获取所有Section名称
List<string> GetSections()
4.4.2 写入方法
csharp 复制代码
// 写入单个配置
bool Write(string section, string key, object value)

// 批量写入一个Section的多个键值对
void WriteSection(string section, Dictionary<string, object> keyValues)
4.4.3 删除方法
csharp 复制代码
// 删除指定键
bool DeleteKey(string section, string key)

// 删除指定Section
bool DeleteSection(string section)
4.4.4 检查方法
csharp 复制代码
// 检查键是否存在
bool KeyExists(string section, string key)

// 检查Section是否存在
bool SectionExists(string section)
4.4.5 属性
csharp 复制代码
// 获取INI文件路径
string FilePath { get; }
4.4.6 使用示例
csharp 复制代码
var config = SystemConfigService.Instance.Config;

// ========== 读取 ==========

// 读取字符串
string ip = config.Read("ModbusTCP", "IP", "127.0.0.1");

// 读取整数
int port = config.ReadInt("ModbusTCP", "Port", 502);

// 读取布尔值
bool activate = config.ReadBool("ModbusTCP", "Activate", false);

// 读取整个Section
var allSettings = config.ReadSection("ModbusTCP");
// 结果: {"IP": "192.168.1.100", "Port": "502", "SlaveId": "1"}

// ========== 写入 ==========

// 写入单个配置
config.Write("ModbusTCP", "IP", "192.168.1.100");
config.Write("ModbusTCP", "Port", 502);
config.Write("ModbusTCP", "Activate", true);

// 批量写入
config.WriteSection("ModbusTCP", new Dictionary<string, object>
{
    { "IP", "192.168.1.100" },
    { "Port", 502 },
    { "SlaveId", 1 },
    { "Activate", true }
});

// ========== 删除 ==========

config.DeleteKey("ModbusTCP", "OldKey");
config.DeleteSection("OldSection");

// ========== 检查 ==========

bool hasKey = config.KeyExists("ModbusTCP", "IP");        // true
bool hasSection = config.SectionExists("ModbusTCP");     // true

4.5 日志清理服务

通过 SystemConfigService.Instance.CleanOldLogs(...) 访问。

4.5.1 清理结果类
csharp 复制代码
public class CleanResult
{
    public bool Success { get; set; }              // 是否成功
    public DateTime CleanTime { get; set; }        // 清理时间
    public int LogFilesDeleted { get; set; }       // 删除的Log文件数
    public long LogSpaceFreed { get; set; }       // 释放的Log空间(字节)
    public int XmlFilesDeleted { get; set; }       // 删除的XML文件数
    public long XmlSpaceFreed { get; set; }       // 释放的XML空间(字节)
    public List<string> LogErrors { get; set; }    // Log清理错误列表
    public List<string> XmlErrors { get; set; }    // XML清理错误列表
    public int TotalFilesDeleted { get; }          // 总删除文件数
    public long TotalSpaceFreed { get; }          // 总释放空间
    public string GetSummary() { }                 // 格式化摘要
}
4.5.2 使用示例
csharp 复制代码
// 清理30天前的文本日志和XML日志
var result = SystemConfigService.Instance.CleanOldLogs(
    LogRetentionDays.Days30,  // 文本日志保留30天
    LogRetentionDays.Days60   // XML日志保留60天
);

if (result.Success)
{
    MessageBox.Show(result.GetSummary());
    // 示例输出: "清理完成: 删除 15 个文件, 释放 25.6 MB 空间"
}

// 或使用整数
var result2 = SystemConfigService.Instance.CleanOldLogs(
    logRetentionDays: 30,
    xmlRetentionDays: 60
);

6. 集成指南

5.1 将类库打包为DLL

步骤1:创建类库项目
  1. 在 Visual Studio 中创建新的 类库(.NET Framework) 项目
  2. 项目命名:Related_Log
  3. 目标框架:.NET Framework 4.8(或你的项目使用的版本)
步骤2:复制源文件

将以下文件复制到类库项目中:

复制代码
Related_Log/
├── Models/
│   └── LogModels.cs
├── Service/
│   ├── Related_Log/
│   │   ├── LogFileService.cs
│   │   └── LogQueueProcessor.cs
│   ├── Related_Xml/
│   │   └── XmlLogService.cs
│   ├── Related_Ini/
│   │   └── IniConfigService.cs
│   └── Related_Clean/
│       └── LogCleanService.cs
└── SystemConfigService.cs
步骤3:添加引用

项目需要引用:

  • System(默认)
  • System.Core(默认)
  • System.Windows.Forms(需要添加)
步骤4:编译生成DLL
  1. 选择 Release 配置
  2. 点击 生成 → 生成解决方案
  3. bin\Release\ 目录下找到 Related_Log.dll

5.2 在新项目中集成

步骤1:添加DLL引用
  1. 在目标项目中,右键 引用 → 添加引用
  2. 选择 浏览 ,找到 Related_Log.dll
  3. 勾选并确定
步骤2:添加命名空间
csharp 复制代码
using Related_Log;
using Related_Log.Models;
步骤3:初始化(可选)
csharp 复制代码
// 在程序启动时访问一次,确保单例初始化
var service = SystemConfigService.Instance;

// 可选:订阅日志事件用于实时显示
service.LogMessage += OnLogMessage;
service.XmlMessage += OnXmlMessage;
步骤4:开始使用
csharp 复制代码
// INI配置
string ip = SystemConfigService.Instance.Config.Read("Network", "IP", "127.0.0.1");

// 文本日志
SystemConfigService.Instance.Log.Info("程序启动", "Program");

// XML日志
SystemConfigService.Instance.Xml.Log(button1, "Admin");

5.3 完整集成示例

示例1:简单控制台应用
csharp 复制代码
using System;
using Related_Log;

namespace MyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // 记录启动
            SystemConfigService.Instance.Log.Info("程序启动", "Program");

            try
            {
                // 读取配置
                var config = SystemConfigService.Instance.Config;
                string ip = config.Read("Server", "IP", "127.0.0.1");
                int port = config.ReadInt("Server", "Port", 8080);

                Console.WriteLine($"服务器地址: {ip}:{port}");

                // 保存配置
                config.Write("Server", "LastConnect", DateTime.Now.ToString());

                // 记录日志
                SystemConfigService.Instance.Log.Info($"连接到服务器 {ip}:{port}", "Program");
            }
            catch (Exception ex)
            {
                SystemConfigService.Instance.Log.Error("程序运行出错", ex, "Program");
            }

            // 清理旧日志
            var result = SystemConfigService.Instance.CleanOldLogs(
                LogRetentionDays.Days30,
                LogRetentionDays.Days30
            );
            Console.WriteLine(result.GetSummary());
        }
    }
}
示例2:WinForms 应用
csharp 复制代码
using System;
using System.Windows.Forms;
using Related_Log;
using Related_Log.Models;

namespace MyWinFormsApp
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();

            // 订阅日志事件,实时显示在界面上
            SystemConfigService.Instance.LogMessage += OnLogMessage;
            SystemConfigService.Instance.XmlMessage += OnXmlMessage;
        }

        private void OnLogMessage(object sender, LogEventArgs e)
        {
            // 跨线程更新UI
            if (txtLog.InvokeRequired)
            {
                txtLog.Invoke(new Action(() => OnLogMessage(sender, e)));
                return;
            }

            txtLog.AppendText($"[{e.Level}] {e.Message}\r\n");
        }

        private void OnXmlMessage(object sender, XmlLogEventArgs e)
        {
            if (txtXml.InvokeRequired)
            {
                txtXml.Invoke(new Action(() => OnXmlMessage(sender, e)));
                return;
            }

            txtXml.AppendText($"[{e.Timestamp:HH:mm:ss}] {e.Username} - {e.Action}\r\n");
        }

        private void btnConnect_Click(object sender, EventArgs e)
        {
            try
            {
                // 记录操作
                SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);

                // 读取配置
                var config = SystemConfigService.Instance.Config;
                string ip = config.Read("PLC", "IP", "192.168.1.1");

                // 连接逻辑...
                SystemConfigService.Instance.Log.Info($"PLC连接成功: {ip}", "MainForm");
            }
            catch (Exception ex)
            {
                SystemConfigService.Instance.Log.Error("PLC连接失败", ex, "MainForm");
            }
        }

        private void btnClearLogs_Click(object sender, EventArgs e)
        {
            var result = SystemConfigService.Instance.CleanOldLogs(7, 30);
            MessageBox.Show(result.GetSummary(), "清理结果");
            SystemConfigService.Instance.Xml.Log(sender, Environment.UserName);
        }
    }
}

5.3 配置文件模板

创建默认的 config.ini 文件(可选):

ini 复制代码
; ============================================
; 系统配置文件
; ============================================

[Network]
; 服务器IP地址
IP=127.0.0.1
; 服务器端口
Port=8080

[PLC]
; PLC通讯配置
IP=192.168.1.1
Port=502
SlaveId=1
; 是否启用PLC通讯
Activate=False

[Storage]
; 日志保留天数
Save_Log=30天
Save_Xml=30天

7. 常见问题FAQ

Q1: 日志写入会影响程序性能吗?

A: 不会。日志写入采用异步队列机制:

  1. 调用日志方法时,只是将日志放入内存队列(微秒级)
  2. 后台线程定期批量写入文件,不阻塞主线程
  3. 即使文件写入失败,也不会影响主程序运行

Q2: 程序意外关闭,队列中的日志会丢失吗?

A: 是的,队列中的日志会丢失。如果需要确保日志不丢失:

  1. 在程序退出时调用 SystemConfigService.Instance.Dispose() 刷新队列
  2. 或在关键操作后调用强制刷新(需要扩展接口)
csharp 复制代码
protected override void OnFormClosing(FormClosingEventArgs e)
{
    SystemConfigService.Instance.Dispose();  // 刷新队列
    base.OnFormClosing(e);
}

Q3: 如何在界面上实时显示日志?

A : 订阅 LogMessageXmlMessage 事件:

csharp 复制代码
SystemConfigService.Instance.LogMessage += (s, e) =>
{
    // 使用 Invoke 确保线程安全
    if (txtLog.InvokeRequired)
    {
        txtLog.Invoke(new Action(() => txtLog.AppendText(e.Message + "\r\n")));
    }
    else
    {
        txtLog.AppendText(e.Message + "\r\n");
    }
};

Q4: INI文件读写失败怎么办?

A: 检查以下几点:

  1. 文件路径权限:确保程序对 INI 文件所在目录有读写权限
  2. 文件编码:确保 INI 文件使用 UTF-8 编码保存
  3. 调用异常Read 方法失败时会抛出异常,建议加 try-catch
csharp 复制代码
try
{
    string value = config.Read("Section", "Key", "default");
}
catch (Exception ex)
{
    SystemConfigService.Instance.Log.Error("读取配置失败", ex, "Config");
}

Q5: 如何自定义日志文件存储路径?

A : 当前版本日志路径固定为应用程序目录下的 logs/logs_xml/。如需自定义,需要修改以下类:

  • LogFileService.cs:修改 _logBaseDir 初始化
  • XmlLogService.cs:修改 _logDirectory 初始化
  • LogCleanService.cs:修改对应的路径

Q6: 多个程序可以同时读写同一个 INI 文件吗?

A: 技术上可以,但不推荐。INI 文件没有内置的并发控制机制。

解决方案

  1. 每个程序使用独立的 INI 文件
  2. 或使用数据库等支持并发的配置存储方式

Q7: 日志文件会无限增大吗?

A: 默认不会,需要手动调用清理方法。建议:

  1. 在程序启动时定期清理
  2. 或在配置页面提供清理按钮
csharp 复制代码
// 程序启动时清理30天前的日志
SystemConfigService.Instance.CleanOldLogs(LogRetentionDays.Days30, LogRetentionDays.Days30);

8. 附录

7.1 命名空间汇总

命名空间 说明
Related_Log 主命名空间,包含 SystemConfigService
Related_Log.Models 数据模型类(LogEntryXmlLogEntry、事件参数等)
Related_Log.Service.Related_Log 文本日志服务(内部)
Related_Log.Service.Related_Xml XML日志服务(内部)
Related_Log.Service.Related_Ini INI配置服务(内部)
Related_Log.Service.Related_Clean 日志清理服务(内部)

7.2 类型汇总

类型 命名空间 说明
SystemConfigService Related_Log 主入口类(单例)
LogService Related_Log 文本日志封装类
XmlLogServiceWrapper Related_Log XML日志封装类
IniConfigService Related_Log.Service.Related_Ini INI配置服务
LogLevel Related_Log.Models 日志级别枚举
LogRetentionDays Related_Log.Models 日志保留天数枚举
LogEntry Related_Log.Models 文本日志实体
XmlLogEntry Related_Log.Models XML日志实体
LogEventArgs Related_Log.Models 日志事件参数
XmlLogEventArgs Related_Log.Models XML日志事件参数
LogCleanService.CleanResult Related_Log.Service.Related_Clean 清理结果类

7.3 快速参考卡片

复制代码
╔═══════════════════════════════════════════════════════════════╗
║                    Related_Log 快速参考                         ║
╠═══════════════════════════════════════════════════════════════╣
║                                                                ║
║  🔹 INI配置                                                    ║
║     var config = SystemConfigService.Instance.Config;         ║
║     config.Read(section, key, defaultValue)                  ║
║     config.Write(section, key, value)                        ║
║     config.ReadInt/ReadBool/ReadDouble...                    ║
║                                                                ║
║  🔹 文本日志                                                   ║
║     SystemConfigService.Instance.Log.Info("消息", "来源")    ║
║     .Warn("警告")                                             ║
║     .Error("错误", exception)                                 ║
║     .Debug("调试")                                            ║
║     .Fatal("致命")                                            ║
║                                                                ║
║  🔹 XML操作日志                                                ║
║     SystemConfigService.Instance.Xml.Log(sender, "用户名")   ║
║     .Log("用户名", "操作", "详情")                            ║
║                                                                ║
║  🔹 日志清理                                                   ║
║     SystemConfigService.Instance.CleanOldLogs(               ║
║         LogRetentionDays.Days30,  // 文本日志30天              ║
║         LogRetentionDays.Days60   // XML日志60天              ║
║     )                                                          ║
║                                                                ║
║  🔹 事件订阅                                                   ║
║     SystemConfigService.Instance.LogMessage += OnLog;         ║
║     SystemConfigService.Instance.XmlMessage += OnXml;        ║
║                                                                ║
╚═══════════════════════════════════════════════════════════════╝

7.4 版本历史

版本 日期 说明
1.0 2026-06-11 初始版本,包含INI、Log、XML三大核心功能

相关推荐
张小姐的猫7 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
杨充10 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充10 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充10 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充10 小时前
2.面向对象的特性
设计模式
杨充10 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充10 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充11 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯14 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法