开发一个面向 ERP 物料层面的 API 后台型中间件服务接口,核心目标是支撑多个控制端(如车间物料终端、产线管控端)的物料数据操作,同时为汇总控制台提供统一的物料数据汇总、监控能力。我会基于.NET 8 架构,从设计思路、核心功能到完整代码实现,给出可直接落地的解决方案。

一、核心设计思路

针对 “多控制端 + 汇总控制台” 的工作模式,该中间件需满足以下核心诉求:

  1. 多控制端(边缘端):提供物料查询、领用、退料、库存预警上报等细粒度 API,支持多终端并发调用,按终端 / 产线维度做数据隔离;
  2. 汇总控制台(中心端):提供物料数据汇总、全局库存监控、异常告警、操作日志审计等聚合型 API,支持控制台统一管控;
  3. 数据一致性:保证多控制端操作后数据实时同步到汇总层,同时基于 ERP(SAP)的物料主数据做统一校验;
  4. 高可用:支持控制端断网重连、数据缓存补发,避免终端操作丢失。
技术架构(分层 + 微服务思想)
层级 核心组件 功能说明
接口层 ASP.NET Core Web API 暴露 RESTful 接口,区分控制端 / 控制台 API 分组
业务层 控制端服务 / 控制台服务 处理终端细粒度操作、控制台汇总逻辑
数据层 ERP 对接服务 / 本地缓存 / 日志存储 对接 SAP ERP、存储终端缓存数据、操作日志
基础层 权限控制 / 异常处理 / 数据同步 终端权限隔离、全局异常捕获、断网重连同步

二、完整代码实现

1. 项目结构(聚焦物料层面核心)

plaintext

ErpMaterialMiddleware/
├── Controllers/          # API控制器(分控制端/控制台)
│   ├── MaterialControlController.cs  # 多控制端接口(车间/产线)
│   └── MaterialConsoleController.cs  # 汇总控制台接口
├── Models/               # 数据模型
│   ├── Dto/              # 接口DTO(入参/出参)
│   ├── Entity/           # 本地存储实体(缓存/日志)
│   └── Enum/             # 枚举(操作类型/终端类型/异常类型)
├── Services/             # 业务服务层
│   ├── IMaterialControlService.cs    # 控制端业务接口
│   ├── MaterialControlService.cs     # 控制端业务实现
│   ├── IMaterialConsoleService.cs    # 控制台业务接口
│   ├── MaterialConsoleService.cs     # 控制台业务实现
│   ├── IErpMaterialService.cs        # ERP物料对接接口
│   └── ErpMaterialService.cs         # SAP NCO实现
├── Utils/                # 工具类
│   ├── TerminalAuthHelper.cs         # 终端权限校验
│   ├── OfflineDataHelper.cs          # 断网缓存/补发
│   └── ExceptionHandler.cs           # 全局异常处理
├── AppSettings.json      # 配置文件(ERP连接/终端配置)
└── Program.cs            # 启动配置
2. 核心配置(AppSettings.json)

json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ErpConfig": {
    "SapConfig": {
      "AppServerHost": "192.168.1.100",
      "SystemNumber": "00",
      "User": "SAP_USER",
      "Password": "SAP_PWD",
      "Client": "800",
      "Language": "ZH"
    }
  },
  "TerminalConfig": {
    "TerminalList": [
      {
        "TerminalCode": "TERM001",  // 控制端编码(产线1终端)
        "TerminalName": "产线1物料终端",
        "FactoryCode": "F01",
        "LineCode": "L01",
        "AuthKey": "TERM001_2026_KEY", // 终端认证密钥
        "AllowedOperations": ["Query", "Take", "Return"] // 允许的操作
      },
      {
        "TerminalCode": "TERM002",  // 控制端编码(产线2终端)
        "TerminalName": "产线2物料终端",
        "FactoryCode": "F01",
        "LineCode": "L02",
        "AuthKey": "TERM002_2026_KEY",
        "AllowedOperations": ["Query", "Take"]
      }
    ]
  },
  "ConnectionStrings": {
    "LocalDb": "Data Source=ErpMaterial.db" // SQLite:存储缓存/日志
  },
  "OfflineConfig": {
    "CacheExpireHours": 24,  // 离线缓存有效期
    "SyncRetryTimes": 3      // 断网重连重试次数
  }
}
3. 核心模型定义

csharp

运行

// Models/Enum/MaterialOperationEnum.cs
namespace ErpMaterialMiddleware.Models.Enum
{
    /// <summary>
    /// 物料操作类型
    /// </summary>
    public enum MaterialOperationEnum
    {
        Query = 1,    // 查询
        Take = 2,     // 领用
        Return = 3,   // 退料
        Warning = 4   // 库存预警上报
    }

    /// <summary>
    /// 终端操作状态
    /// </summary>
    public enum TerminalOperateStatusEnum
    {
        Success = 1,  // 成功
        Failed = 2,   // 失败
        Offline = 3   // 离线缓存
    }
}

// Models/Dto/MaterialControlDto.cs
using ErpMaterialMiddleware.Models.Enum;
using System.ComponentModel.DataAnnotations;

namespace ErpMaterialMiddleware.Models.Dto
{
    /// <summary>
    /// 控制端物料领用入参
    /// </summary>
    public class MaterialTakeDto
    {
        /// <summary>
        /// 终端编码(多控制端标识)
        /// </summary>
        [Required]
        public string TerminalCode { get; }

        /// <summary>
        /// 终端认证密钥
        /// </summary>
        [Required]
        public string AuthKey { get; }

        /// <summary>
        /// 物料编码
        /// </summary>
        [Required]
        public string MaterialCode { get; }

        /// <summary>
        /// 领用数量
        /// </summary>
        [Required]
        [Range(0.01, double.MaxValue, ErrorMessage = "领用数量必须大于0")]
        public decimal TakeQty { get; }

        /// <summary>
        /// 领用工单
        /// </summary>
        [Required]
        public string OrderNo { get; }

        /// <summary>
        /// 操作人
        /// </summary>
        [Required]
        public string Operator { get; }
    }

    /// <summary>
    /// 控制端物料查询入参
    /// </summary>
    public class MaterialQueryDto
    {
        [Required]
        public string TerminalCode { get; }
        [Required]
        public string AuthKey { get; }
        [Required]
        public string MaterialCode { get; }
        // 可选:工厂/产线筛选
        public string FactoryCode { get; }
        public string LineCode { get; }
    }

    /// <summary>
    /// 物料查询返回结果
    /// </summary>
    public class MaterialQueryResponseDto
    {
        /// <summary>
        /// 物料编码
        /// </summary>
        public string MaterialCode { get; set; }
        /// <summary>
        /// 物料名称
        /// </summary>
        public string MaterialName { get; set; }
        /// <summary>
        /// 规格型号
        /// </summary>
        public string Spec { get; set; }
        /// <summary>
        /// 单位
        /// </summary>
        public string Unit { get; set; }
        /// <summary>
        /// 库存数量
        /// </summary>
        public decimal StockQty { get; set; }
        /// <summary>
        /// 安全库存
        /// </summary>
        public decimal SafeStockQty { get; set; }
        /// <summary>
        /// 库存位置
        /// </summary>
        public string StockLocation { get; set; }
    }

    /// <summary>
    /// 控制台物料汇总查询入参
    /// </summary>
    public class MaterialSummaryQueryDto
    {
        /// <summary>
        /// 控制台管理员Token
        /// </summary>
        [Required]
        public string AdminToken { get; }
        /// <summary>
        /// 工厂编码(可选,为空则查全部)
        /// </summary>
        public string FactoryCode { get; }
        /// <summary>
        /// 统计开始时间
        /// </summary>
        [Required]
        public DateTime StartTime { get; }
        /// <summary>
        /// 统计结束时间
        /// </summary>
        [Required]
        public DateTime EndTime { get; }
    }

    /// <summary>
    /// 控制台物料汇总结果
    /// </summary>
    public class MaterialSummaryResponseDto
    {
        /// <summary>
        /// 工厂编码
        /// </summary>
        public string FactoryCode { get; set; }
        /// <summary>
        /// 物料编码
        /// </summary>
        public string MaterialCode { get; set; }
        /// <summary>
        /// 物料名称
        /// </summary>
        public string MaterialName { get; set; }
        /// <summary>
        /// 总领用数量
        /// </summary>
        public decimal TotalTakeQty { get; set; }
        /// <summary>
        /// 总退料数量
        /// </summary>
        public decimal TotalReturnQty { get; set; }
        /// <summary>
        /// 当前库存
        /// </summary>
        public decimal CurrentStockQty { get; set; }
        /// <summary>
        /// 预警次数
        /// </summary>
        public int WarningCount { get; set; }
        /// <summary>
        /// 操作终端数
        /// </summary>
        public int TerminalCount { get; set; }
    }
}

// Models/Entity/TerminalOperateLog.cs
using ErpMaterialMiddleware.Models.Enum;

namespace ErpMaterialMiddleware.Models.Entity
{
    /// <summary>
    /// 终端操作日志(用于控制台审计/汇总)
    /// </summary>
    public class TerminalOperateLog
    {
        /// <summary>
        /// 主键
        /// </summary>
        public int Id { get; set; }
        /// <summary>
        /// 终端编码
        /// </summary>
        public string TerminalCode { get; set; }
        /// <summary>
        /// 工厂编码
        /// </summary>
        public string FactoryCode { get; set; }
        /// <summary>
        /// 产线编码
        /// </summary>
        public string LineCode { get; set; }
        /// <summary>
        /// 物料编码
        /// </summary>
        public string MaterialCode { get; set; }
        /// <summary>
        /// 操作类型
        /// </summary>
        public MaterialOperationEnum OperationType { get; set; }
        /// <summary>
        /// 操作数量
        /// </summary>
        public decimal OperateQty { get; set; }
        /// <summary>
        /// 工单编号
        /// </summary>
        public string OrderNo { get; set; }
        /// <summary>
        /// 操作人
        /// </summary>
        public string Operator { get; set; }
        /// <summary>
        /// 操作时间
        /// </summary>
        public DateTime OperateTime { get; set; }
        /// <summary>
        /// 操作状态
        /// </summary>
        public TerminalOperateStatusEnum Status { get; set; }
        /// <summary>
        /// 异常信息(失败时)
        /// </summary>
        public string ErrorMsg { get; set; }
        /// <summary>
        /// 是否同步到ERP
        /// </summary>
        public bool IsSyncedToErp { get; set; }
    }

    /// <summary>
    /// 离线缓存数据(断网时存储)
    /// </summary>
    public class OfflineMaterialData
    {
        public int Id { get; set; }
        public string TerminalCode { get; set; }
        public string RequestData { get; set; } // JSON格式的请求数据
        public MaterialOperationEnum OperationType { get; set; }
        public DateTime CreateTime { get; set; }
        public int RetryCount { get; set; } // 重试次数
        public bool IsSynced { get; set; } // 是否已同步
    }
}
4. 核心工具类(终端认证 / 离线缓存)

csharp

运行

// Utils/TerminalAuthHelper.cs
using Microsoft.Extensions.Configuration;

namespace ErpMaterialMiddleware.Utils
{
    /// <summary>
    /// 多控制端认证校验工具
    /// </summary>
    public class TerminalAuthHelper
    {
        private readonly IConfiguration _configuration;
        private readonly List<TerminalConfigModel> _terminalList;

        public TerminalAuthHelper(IConfiguration configuration)
        {
            _configuration = configuration;
            // 加载终端配置
            _terminalList = _configuration.GetSection("TerminalConfig:TerminalList")
                .Get<List<TerminalConfigModel>>() ?? new List<TerminalConfigModel>();
        }

        /// <summary>
        /// 校验终端认证密钥
        /// </summary>
        /// <param name="terminalCode">终端编码</param>
        /// <param name="authKey">认证密钥</param>
        /// <returns>是否认证通过</returns>
        public bool ValidateTerminalAuth(string terminalCode, string authKey)
        {
            var terminal = _terminalList.FirstOrDefault(t => t.TerminalCode == terminalCode);
            if (terminal == null) return false;
            return terminal.AuthKey == authKey;
        }

        /// <summary>
        /// 校验终端操作权限
        /// </summary>
        /// <param name="terminalCode">终端编码</param>
        /// <param name="operation">操作类型</param>
        /// <returns>是否有权限</returns>
        public bool CheckTerminalOperationPermission(string terminalCode, string operation)
        {
            var terminal = _terminalList.FirstOrDefault(t => t.TerminalCode == terminalCode);
            if (terminal == null) return false;
            return terminal.AllowedOperations.Contains(operation);
        }

        /// <summary>
        /// 获取终端关联的工厂/产线
        /// </summary>
        public (string FactoryCode, string LineCode) GetTerminalInfo(string terminalCode)
        {
            var terminal = _terminalList.FirstOrDefault(t => t.TerminalCode == terminalCode);
            return terminal == null ? (string.Empty, string.Empty) : (terminal.FactoryCode, terminal.LineCode);
        }

        // 终端配置模型
        public class TerminalConfigModel
        {
            public string TerminalCode { get; set; }
            public string TerminalName { get; set; }
            public string FactoryCode { get; set; }
            public string LineCode { get; set; }
            public string AuthKey { get; set; }
            public List<string> AllowedOperations { get; set; }
        }
    }

    /// <summary>
    /// 控制台管理员认证(简化版)
    /// </summary>
    public class ConsoleAuthHelper
    {
        // 生产环境建议替换为JWT/数据库存储
        private const string AdminToken = "CONSOLE_ADMIN_2026_TOKEN";

        /// <summary>
        /// 校验控制台管理员Token
        /// </summary>
        public bool ValidateConsoleToken(string token)
        {
            return token == AdminToken;
        }
    }
}

// Utils/OfflineDataHelper.cs
using ErpMaterialMiddleware.Models.Entity;
using ErpMaterialMiddleware.Models.Enum;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using System.Text.Json;

namespace ErpMaterialMiddleware.Utils
{
    /// <summary>
    /// 断网缓存/补发工具
    /// </summary>
    public class OfflineDataHelper
    {
        private readonly string _connectionString;
        private readonly int _syncRetryTimes;
        private readonly int _cacheExpireHours;

        public OfflineDataHelper(IConfiguration configuration)
        {
            _connectionString = configuration.GetConnectionString("LocalDb");
            _syncRetryTimes = configuration.GetValue<int>("OfflineConfig:SyncRetryTimes");
            _cacheExpireHours = configuration.GetValue<int>("OfflineConfig:CacheExpireHours");

            // 初始化离线缓存表
            InitOfflineTable();
        }

        /// <summary>
        /// 初始化离线缓存表
        /// </summary>
        private void InitOfflineTable()
        {
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    CREATE TABLE IF NOT EXISTS OfflineMaterialData (
                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                        TerminalCode TEXT NOT NULL,
                        RequestData TEXT NOT NULL,
                        OperationType INTEGER NOT NULL,
                        CreateTime DATETIME NOT NULL,
                        RetryCount INTEGER DEFAULT 0,
                        IsSynced BOOLEAN DEFAULT 0
                    )", conn);
                cmd.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// 保存离线缓存数据
        /// </summary>
        public void SaveOfflineData(string terminalCode, object requestData, MaterialOperationEnum operationType)
        {
            var requestJson = JsonSerializer.Serialize(requestData);
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    INSERT INTO OfflineMaterialData (TerminalCode, RequestData, OperationType, CreateTime, RetryCount, IsSynced)
                    VALUES (@TerminalCode, @RequestData, @OperationType, @CreateTime, 0, 0)", conn);
                cmd.Parameters.AddWithValue("@TerminalCode", terminalCode);
                cmd.Parameters.AddWithValue("@RequestData", requestJson);
                cmd.Parameters.AddWithValue("@OperationType", (int)operationType);
                cmd.Parameters.AddWithValue("@CreateTime", DateTime.Now);
                cmd.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// 获取待同步的离线数据
        /// </summary>
        public List<OfflineMaterialData> GetUnsyncedData()
        {
            var result = new List<OfflineMaterialData>();
            var expireTime = DateTime.Now.AddHours(-_cacheExpireHours);
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    SELECT Id, TerminalCode, RequestData, OperationType, CreateTime, RetryCount, IsSynced
                    FROM OfflineMaterialData
                    WHERE IsSynced = 0 AND RetryCount < @RetryTimes AND CreateTime > @ExpireTime", conn);
                cmd.Parameters.AddWithValue("@RetryTimes", _syncRetryTimes);
                cmd.Parameters.AddWithValue("@ExpireTime", expireTime);
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        result.Add(new OfflineMaterialData
                        {
                            Id = reader.GetInt32(0),
                            TerminalCode = reader.GetString(1),
                            RequestData = reader.GetString(2),
                            OperationType = (MaterialOperationEnum)reader.GetInt32(3),
                            CreateTime = reader.GetDateTime(4),
                            RetryCount = reader.GetInt32(5),
                            IsSynced = reader.GetBoolean(6)
                        });
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// 更新离线数据同步状态
        /// </summary>
        public void UpdateOfflineDataStatus(int id, bool isSynced, int retryCount = 0)
        {
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    UPDATE OfflineMaterialData
                    SET IsSynced = @IsSynced, RetryCount = @RetryCount
                    WHERE Id = @Id", conn);
                cmd.Parameters.AddWithValue("@IsSynced", isSynced);
                cmd.Parameters.AddWithValue("@RetryCount", retryCount);
                cmd.Parameters.AddWithValue("@Id", id);
                cmd.ExecuteNonQuery();
            }
        }
    }
}
5. ERP 物料对接服务(核心)

csharp

运行

// Services/IErpMaterialService.cs
using ErpMaterialMiddleware.Models.Dto;

namespace ErpMaterialMiddleware.Services
{
    /// <summary>
    /// ERP(SAP)物料对接接口
    /// </summary>
    public interface IErpMaterialService
    {
        /// <summary>
        /// 从ERP查询物料基础信息+库存
        /// </summary>
        Task<MaterialQueryResponseDto> QueryErpMaterialAsync(string materialCode, string factoryCode, string lineCode);

        /// <summary>
        /// 物料领用同步到ERP
        /// </summary>
        Task<bool> SyncMaterialTakeToErpAsync(string materialCode, decimal takeQty, string orderNo, string factoryCode, string lineCode, string operatorName);

        /// <summary>
        /// 物料退料同步到ERP
        /// </summary>
        Task<bool> SyncMaterialReturnToErpAsync(string materialCode, decimal returnQty, string orderNo, string factoryCode, string lineCode, string operatorName);
    }
}

// Services/ErpMaterialService.cs
using ErpMaterialMiddleware.Models.Dto;
using Microsoft.Extensions.Configuration;
using SAP.Middleware.Connector;

namespace ErpMaterialMiddleware.Services
{
    /// <summary>
    /// SAP ERP物料对接实现
    /// </summary>
    public class ErpMaterialService : IErpMaterialService
    {
        private readonly IConfiguration _configuration;

        public ErpMaterialService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        /// <summary>
        /// 构建SAP连接配置
        /// </summary>
        private RfcConfigParameters GetSapConfig()
        {
            var sapConfig = _configuration.GetSection("ErpConfig:SapConfig");
            var config = new RfcConfigParameters();
            config.Add(RfcConfigParameters.AppServerHost, sapConfig["AppServerHost"]);
            config.Add(RfcConfigParameters.SystemNumber, sapConfig["SystemNumber"]);
            config.Add(RfcConfigParameters.User, sapConfig["User"]);
            config.Add(RfcConfigParameters.Password, sapConfig["Password"]);
            config.Add(RfcConfigParameters.Client, sapConfig["Client"]);
            config.Add(RfcConfigParameters.Language, sapConfig["Language"]);
            return config;
        }

        /// <summary>
        /// 查询SAP物料数据
        /// </summary>
        public async Task<MaterialQueryResponseDto> QueryErpMaterialAsync(string materialCode, string factoryCode, string lineCode)
        {
            return await Task.Run(() =>
            {
                var result = new MaterialQueryResponseDto();
                try
                {
                    using (var destination = RfcDestinationManager.GetDestination(GetSapConfig()))
                    using (var repository = new RfcRepository(destination))
                    {
                        // 调用SAP自定义物料查询RFC(推荐自定义,性能更优)
                        IRfcFunction rfcFunction = repository.CreateFunction("Z_MATERIAL_QUERY");
                        rfcFunction.SetValue("I_MATNR", materialCode); // 物料编码
                        rfcFunction.SetValue("I_WERKS", factoryCode);   // 工厂编码
                        rfcFunction.SetValue("I_LINE", lineCode);       // 产线编码

                        rfcFunction.Invoke(destination);

                        // 获取物料基础信息
                        IRfcStructure matStruct = rfcFunction.GetStructure("ES_MATERIAL");
                        result.MaterialCode = matStruct.GetString("MATNR");
                        result.MaterialName = matStruct.GetString("MAKTX");
                        result.Spec = matStruct.GetString("SPECS");
                        result.Unit = matStruct.GetString("MEINS");
                        result.SafeStockQty = matStruct.GetDecimal("SSTOCK");

                        // 获取库存信息
                        IRfcStructure stockStruct = rfcFunction.GetStructure("ES_STOCK");
                        result.StockQty = stockStruct.GetDecimal("STOCK_QTY");
                        result.StockLocation = stockStruct.GetString("LGORT");
                    }
                }
                catch (RfcException ex)
                {
                    throw new Exception($"SAP物料查询失败:{ex.Message},错误码:{ex.RfcErrorCode}");
                }
                return result;
            });
        }

        /// <summary>
        /// 同步物料领用到SAP
        /// </summary>
        public async Task<bool> SyncMaterialTakeToErpAsync(string materialCode, decimal takeQty, string orderNo, string factoryCode, string lineCode, string operatorName)
        {
            return await Task.Run(() =>
            {
                try
                {
                    using (var destination = RfcDestinationManager.GetDestination(GetSapConfig()))
                    using (var repository = new RfcRepository(destination))
                    {
                        IRfcFunction rfcFunction = repository.CreateFunction("Z_MATERIAL_TAKE");
                        rfcFunction.SetValue("I_MATNR", materialCode);
                        rfcFunction.SetValue("I_MENGE", takeQty);
                        rfcFunction.SetValue("I_AUFNR", orderNo);
                        rfcFunction.SetValue("I_WERKS", factoryCode);
                        rfcFunction.SetValue("I_LINE", lineCode);
                        rfcFunction.SetValue("I_OPERATOR", operatorName);

                        rfcFunction.Invoke(destination);

                        // 获取执行结果
                        string resultCode = rfcFunction.GetString("E_CODE");
                        string resultMsg = rfcFunction.GetString("E_MSG");
                        if (resultCode != "000")
                        {
                            throw new Exception($"SAP领用同步失败:{resultMsg}");
                        }
                        return true;
                    }
                }
                catch (RfcException ex)
                {
                    throw new Exception($"SAP RFC调用失败:{ex.Message},错误码:{ex.RfcErrorCode}");
                }
            });
        }

        /// <summary>
        /// 同步物料退料到SAP
        /// </summary>
        public async Task<bool> SyncMaterialReturnToErpAsync(string materialCode, decimal returnQty, string orderNo, string factoryCode, string lineCode, string operatorName)
        {
            // 逻辑同领用,替换为退料RFC(Z_MATERIAL_RETURN)
            return await Task.Run(() =>
            {
                try
                {
                    using (var destination = RfcDestinationManager.GetDestination(GetSapConfig()))
                    using (var repository = new RfcRepository(destination))
                    {
                        IRfcFunction rfcFunction = repository.CreateFunction("Z_MATERIAL_RETURN");
                        rfcFunction.SetValue("I_MATNR", materialCode);
                        rfcFunction.SetValue("I_MENGE", returnQty);
                        rfcFunction.SetValue("I_AUFNR", orderNo);
                        rfcFunction.SetValue("I_WERKS", factoryCode);
                        rfcFunction.SetValue("I_LINE", lineCode);
                        rfcFunction.SetValue("I_OPERATOR", operatorName);

                        rfcFunction.Invoke(destination);

                        string resultCode = rfcFunction.GetString("E_CODE");
                        string resultMsg = rfcFunction.GetString("E_MSG");
                        if (resultCode != "000")
                        {
                            throw new Exception($"SAP退料同步失败:{resultMsg}");
                        }
                        return true;
                    }
                }
                catch (RfcException ex)
                {
                    throw new Exception($"SAP RFC调用失败:{ex.Message},错误码:{ex.RfcErrorCode}");
                }
            });
        }
    }
}
6. 控制端 / 控制台业务服务

csharp

运行

// Services/IMaterialControlService.cs
using ErpMaterialMiddleware.Models.Dto;
using ErpMaterialMiddleware.Models.Entity;
using ErpMaterialMiddleware.Models.Enum;

namespace ErpMaterialMiddleware.Services
{
    /// <summary>
    /// 多控制端物料业务接口
    /// </summary>
    public interface IMaterialControlService
    {
        /// <summary>
        /// 物料查询
        /// </summary>
        Task<MaterialQueryResponseDto> QueryMaterialAsync(MaterialQueryDto dto);

        /// <summary>
        /// 物料领用
        /// </summary>
        Task<bool> TakeMaterialAsync(MaterialTakeDto dto, out string errorMsg);

        /// <summary>
        /// 记录终端操作日志
        /// </summary>
        void RecordOperateLog(TerminalOperateLog log);

        /// <summary>
        /// 离线数据补发(后台任务)
        /// </summary>
        Task SyncOfflineDataAsync();
    }
}

// Services/MaterialControlService.cs
using ErpMaterialMiddleware.Models.Dto;
using ErpMaterialMiddleware.Models.Entity;
using ErpMaterialMiddleware.Models.Enum;
using ErpMaterialMiddleware.Utils;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;

namespace ErpMaterialMiddleware.Services
{
    public class MaterialControlService : IMaterialControlService
    {
        private readonly IErpMaterialService _erpMaterialService;
        private readonly TerminalAuthHelper _terminalAuthHelper;
        private readonly OfflineDataHelper _offlineDataHelper;
        private readonly string _connectionString;

        public MaterialControlService(IErpMaterialService erpMaterialService,
            TerminalAuthHelper terminalAuthHelper,
            OfflineDataHelper offlineDataHelper,
            IConfiguration configuration)
        {
            _erpMaterialService = erpMaterialService;
            _terminalAuthHelper = terminalAuthHelper;
            _offlineDataHelper = offlineDataHelper;
            _connectionString = configuration.GetConnectionString("LocalDb");

            // 初始化操作日志表
            InitOperateLogTable();
        }

        /// <summary>
        /// 初始化操作日志表
        /// </summary>
        private void InitOperateLogTable()
        {
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    CREATE TABLE IF NOT EXISTS TerminalOperateLog (
                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                        TerminalCode TEXT NOT NULL,
                        FactoryCode TEXT NOT NULL,
                        LineCode TEXT NOT NULL,
                        MaterialCode TEXT NOT NULL,
                        OperationType INTEGER NOT NULL,
                        OperateQty DECIMAL NOT NULL,
                        OrderNo TEXT NOT NULL,
                        Operator TEXT NOT NULL,
                        OperateTime DATETIME NOT NULL,
                        Status INTEGER NOT NULL,
                        ErrorMsg TEXT
                    )", conn);
                cmd.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// 物料查询
        /// </summary>
        public async Task<MaterialQueryResponseDto> QueryMaterialAsync(MaterialQueryDto dto)
        {
            // 1. 终端认证
            if (!_terminalAuthHelper.ValidateTerminalAuth(dto.TerminalCode, dto.AuthKey))
            {
                throw new Exception("终端认证失败,请检查终端编码和认证密钥");
            }

            // 2. 校验查询权限
            if (!_terminalAuthHelper.CheckTerminalOperationPermission(dto.TerminalCode, "Query"))
            {
                throw new Exception("该终端无物料查询权限");
            }

            // 3. 获取终端关联的工厂/产线
            var (factoryCode, lineCode) = _terminalAuthHelper.GetTerminalInfo(dto.TerminalCode);
            factoryCode = string.IsNullOrEmpty(dto.FactoryCode) ? factoryCode : dto.FactoryCode;
            lineCode = string.IsNullOrEmpty(dto.LineCode) ? lineCode : dto.LineCode;

            // 4. 调用ERP查询物料
            var materialData = await _erpMaterialService.QueryErpMaterialAsync(dto.MaterialCode, factoryCode, lineCode);

            // 5. 记录查询日志
            RecordOperateLog(new TerminalOperateLog
            {
                TerminalCode = dto.TerminalCode,
                FactoryCode = factoryCode,
                LineCode = lineCode,
                MaterialCode = dto.MaterialCode,
                OperationType = MaterialOperationEnum.Query,
                OperateQty = 0,
                OrderNo = string.Empty,
                Operator = string.Empty,
                OperateTime = DateTime.Now,
                Status = TerminalOperateStatusEnum.Success,
                ErrorMsg = string.Empty
            });

            return materialData;
        }

        /// <summary>
        /// 物料领用
        /// </summary>
        public async Task<bool> TakeMaterialAsync(MaterialTakeDto dto, out string errorMsg)
        {
            errorMsg = string.Empty;
            try
            {
                // 1. 终端认证
                if (!_terminalAuthHelper.ValidateTerminalAuth(dto.TerminalCode, dto.AuthKey))
                {
                    errorMsg = "终端认证失败";
                    return false;
                }

                // 2. 校验领用权限
                if (!_terminalAuthHelper.CheckTerminalOperationPermission(dto.TerminalCode, "Take"))
                {
                    errorMsg = "该终端无物料领用权限";
                    return false;
                }

                // 3. 获取终端关联的工厂/产线
                var (factoryCode, lineCode) = _terminalAuthHelper.GetTerminalInfo(dto.TerminalCode);

                // 4. 调用ERP同步领用数据
                bool syncSuccess = false;
                try
                {
                    syncSuccess = await _erpMaterialService.SyncMaterialTakeToErpAsync(
                        dto.MaterialCode, dto.TakeQty, dto.OrderNo,
                        factoryCode, lineCode, dto.Operator);
                }
                catch (Exception ex)
                {
                    // 断网/ERP异常:保存离线缓存
                    _offlineDataHelper.SaveOfflineData(dto.TerminalCode, dto, MaterialOperationEnum.Take);
                    errorMsg = $"ERP同步失败,已保存离线缓存:{ex.Message}";
                    // 记录离线日志
                    RecordOperateLog(new TerminalOperateLog
                    {
                        TerminalCode = dto.TerminalCode,
                        FactoryCode = factoryCode,
                        LineCode = lineCode,
                        MaterialCode = dto.MaterialCode,
                        OperationType = MaterialOperationEnum.Take,
                        OperateQty = dto.TakeQty,
                        OrderNo = dto.OrderNo,
                        Operator = dto.Operator,
                        OperateTime = DateTime.Now,
                        Status = TerminalOperateStatusEnum.Offline,
                        ErrorMsg = errorMsg
                    });
                    return true; // 终端侧返回成功(离线缓存)
                }

                // 5. 记录操作日志
                RecordOperateLog(new TerminalOperateLog
                {
                    TerminalCode = dto.TerminalCode,
                    FactoryCode = factoryCode,
                    LineCode = lineCode,
                    MaterialCode = dto.MaterialCode,
                    OperationType = MaterialOperationEnum.Take,
                    OperateQty = dto.TakeQty,
                    OrderNo = dto.OrderNo,
                    Operator = dto.Operator,
                    OperateTime = DateTime.Now,
                    Status = syncSuccess ? TerminalOperateStatusEnum.Success : TerminalOperateStatusEnum.Failed,
                    ErrorMsg = syncSuccess ? string.Empty : "ERP同步失败"
                });

                return syncSuccess;
            }
            catch (Exception ex)
            {
                errorMsg = $"领用操作异常:{ex.Message}";
                return false;
            }
        }

        /// <summary>
        /// 记录终端操作日志
        /// </summary>
        public void RecordOperateLog(TerminalOperateLog log)
        {
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    INSERT INTO TerminalOperateLog (
                        TerminalCode, FactoryCode, LineCode, MaterialCode, OperationType,
                        OperateQty, OrderNo, Operator, OperateTime, Status, ErrorMsg)
                    VALUES (
                        @TerminalCode, @FactoryCode, @LineCode, @MaterialCode, @OperationType,
                        @OperateQty, @OrderNo, @Operator, @OperateTime, @Status, @ErrorMsg)", conn);
                cmd.Parameters.AddWithValue("@TerminalCode", log.TerminalCode);
                cmd.Parameters.AddWithValue("@FactoryCode", log.FactoryCode);
                cmd.Parameters.AddWithValue("@LineCode", log.LineCode);
                cmd.Parameters.AddWithValue("@MaterialCode", log.MaterialCode);
                cmd.Parameters.AddWithValue("@OperationType", (int)log.OperationType);
                cmd.Parameters.AddWithValue("@OperateQty", (double)log.OperateQty);
                cmd.Parameters.AddWithValue("@OrderNo", log.OrderNo);
                cmd.Parameters.AddWithValue("@Operator", log.Operator);
                cmd.Parameters.AddWithValue("@OperateTime", log.OperateTime);
                cmd.Parameters.AddWithValue("@Status", (int)log.Status);
                cmd.Parameters.AddWithValue("@ErrorMsg", log.ErrorMsg ?? string.Empty);
                cmd.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// 离线数据补发
        /// </summary>
        public async Task SyncOfflineDataAsync()
        {
            var unsyncedData = _offlineDataHelper.GetUnsyncedData();
            foreach (var data in unsyncedData)
            {
                try
                {
                    // 反序列化请求数据
                    if (data.OperationType == MaterialOperationEnum.Take)
                    {
                        var takeDto = System.Text.Json.JsonSerializer.Deserialize<MaterialTakeDto>(data.RequestData);
                        if (takeDto != null)
                        {
                            string errorMsg;
                            var (factoryCode, lineCode) = _terminalAuthHelper.GetTerminalInfo(data.TerminalCode);
                            // 重新同步到ERP
                            bool syncSuccess = await _erpMaterialService.SyncMaterialTakeToErpAsync(
                                takeDto.MaterialCode, takeDto.TakeQty, takeDto.OrderNo,
                                factoryCode, lineCode, takeDto.Operator);
                            if (syncSuccess)
                            {
                                // 更新同步状态
                                _offlineDataHelper.UpdateOfflineDataStatus(data.Id, true);
                                // 更新操作日志状态
                                UpdateOperateLogStatus(takeDto.TerminalCode, takeDto.MaterialCode, takeDto.OrderNo, MaterialOperationEnum.Take, TerminalOperateStatusEnum.Success);
                            }
                            else
                            {
                                // 重试次数+1
                                _offlineDataHelper.UpdateOfflineDataStatus(data.Id, false, data.RetryCount + 1);
                            }
                        }
                    }
                }
                catch (Exception)
                {
                    // 重试次数+1
                    _offlineDataHelper.UpdateOfflineDataStatus(data.Id, false, data.RetryCount + 1);
                }
            }
        }

        /// <summary>
        /// 更新操作日志状态
        /// </summary>
        private void UpdateOperateLogStatus(string terminalCode, string materialCode, string orderNo, MaterialOperationEnum operationType, TerminalOperateStatusEnum status)
        {
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                var cmd = new SqliteCommand(@"
                    UPDATE TerminalOperateLog
                    SET Status = @Status, ErrorMsg = ''
                    WHERE TerminalCode = @TerminalCode 
                      AND MaterialCode = @MaterialCode 
                      AND OrderNo = @OrderNo 
                      AND OperationType = @OperationType 
                      AND Status = @OldStatus", conn);
                cmd.Parameters.AddWithValue("@Status", (int)status);
                cmd.Parameters.AddWithValue("@TerminalCode", terminalCode);
                cmd.Parameters.AddWithValue("@MaterialCode", materialCode);
                cmd.Parameters.AddWithValue("@OrderNo", orderNo);
                cmd.Parameters.AddWithValue("@OperationType", (int)operationType);
                cmd.Parameters.AddWithValue("@OldStatus", (int)TerminalOperateStatusEnum.Offline);
                cmd.ExecuteNonQuery();
            }
        }
    }
}

// Services/IMaterialConsoleService.cs
using ErpMaterialMiddleware.Models.Dto;

namespace ErpMaterialMiddleware.Services
{
    /// <summary>
    /// 汇总控制台物料业务接口
    /// </summary>
    public interface IMaterialConsoleService
    {
        /// <summary>
        /// 获取物料操作汇总数据
        /// </summary>
        Task<List<MaterialSummaryResponseDto>> GetMaterialSummaryAsync(MaterialSummaryQueryDto dto);

        /// <summary>
        /// 获取终端操作日志(审计)
        /// </summary>
        Task<List<Models.Entity.TerminalOperateLog>> GetTerminalOperateLogAsync(string factoryCode, DateTime startTime, DateTime endTime);

        /// <summary>
        /// 获取库存预警物料列表
        /// </summary>
        Task<List<MaterialQueryResponseDto>> GetStockWarningMaterialsAsync(string factoryCode);
    }
}

// Services/MaterialConsoleService.cs
using ErpMaterialMiddleware.Models.Dto;
using ErpMaterialMiddleware.Models.Entity;
using ErpMaterialMiddleware.Models.Enum;
using ErpMaterialMiddleware.Utils;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;

namespace ErpMaterialMiddleware.Services
{
    public class MaterialConsoleService : IMaterialConsoleService
    {
        private readonly IErpMaterialService _erpMaterialService;
        private readonly ConsoleAuthHelper _consoleAuthHelper;
        private readonly string _connectionString;

        public MaterialConsoleService(IErpMaterialService erpMaterialService,
            ConsoleAuthHelper consoleAuthHelper,
            IConfiguration configuration)
        {
            _erpMaterialService = erpMaterialService;
            _consoleAuthHelper = consoleAuthHelper;
            _connectionString = configuration.GetConnectionString("LocalDb");
        }

        /// <summary>
        /// 获取物料汇总数据
        /// </summary>
        public async Task<List<MaterialSummaryResponseDto>> GetMaterialSummaryAsync(MaterialSummaryQueryDto dto)
        {
            // 1. 控制台认证
            if (!_consoleAuthHelper.ValidateConsoleToken(dto.AdminToken))
            {
                throw new Exception("控制台管理员认证失败");
            }

            var result = new List<MaterialSummaryResponseDto>();
            using (var conn = new SqliteConnection(_connectionString))
            {
                conn.Open();
                // 构建汇总SQL
                string sql = @"
                    SELECT 
                        FactoryCode, MaterialCode,
                        SUM(CASE WHEN OperationType = 2 THEN OperateQty ELSE 0 END) AS TotalTakeQty,
                        SUM(CASE WHEN OperationType = 3 THEN OperateQty ELSE 0 END) AS TotalReturnQty,
                        COUNT(DISTINCT TerminalCode) AS TerminalCount,
                        SUM(CASE WHEN OperationType = 4 THEN 1 ELSE 0 END) AS WarningCount
                    FROM TerminalOperateLog
                    WHERE OperateTime BETWEEN @StartTime AND @EndTime";
                if (!string.IsNullOrEmpty(dto.FactoryCode))
                {
                    sql += " AND FactoryCode = @FactoryCode";
                }
                sql += " GROUP BY FactoryCode, MaterialCode";

                var cmd = new SqliteCommand(sql, conn);
                cmd.Parameters.AddWithValue("@StartTime", dto.StartTime);
                cmd.Parameters.AddWithValue("@EndTime", dto.EndTime);
                if (!string.IsNullOrEmpty(dto.FactoryCode))
                {
                    cmd.Parameters.AddWithValue("@FactoryCode", dto.FactoryCode);
                }

                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        var factoryCode = reader.GetString(0);
                        var materialCode = reader.GetString(1);
                        // 从ERP获取当前库存
                        var materialData = await _erpMaterialService.QueryErpMaterialAsync(materialCode, factoryCode, string.Empty);

                        result.Add(new MaterialSummaryResponseDto
                        {
                            FactoryCode = factoryCode,
                            MaterialCode = materialCode,
                            MaterialName = materialData.MaterialName,
                            TotalTakeQty = (decimal)reader.GetDouble(2),
                            TotalReturnQty = (decimal)reader.GetDouble(3),
                            TerminalCount = reader.GetInt32(4),
                            WarningCount = reader.GetInt32(5),
                            CurrentStockQty = materialData.StockQty
                        });
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// 获取终端操作日志
        /// </summary>
        public async Task<List<TerminalOperateLog>> GetTerminalOperateLogAsync(string factoryCode, DateTime startTime, DateTime endTime)
        {
            // 实际项目中需添加控制台认证
            var result = new List<TerminalOperateLog>();
            await Task.Run(() =>
            {
                using (var conn = new SqliteConnection(_connectionString))
                {
                    conn.Open();
                    string sql = @"
                        SELECT Id, TerminalCode, FactoryCode, LineCode, MaterialCode, OperationType,
                               OperateQty, OrderNo, Operator, OperateTime, Status, ErrorMsg
                        FROM TerminalOperateLog
                        WHERE OperateTime BETWEEN @StartTime AND @EndTime";
                    if (!string.IsNullOrEmpty(factoryCode))
                    {
                        sql += " AND FactoryCode = @FactoryCode";
                    }

                    var cmd = new SqliteCommand(sql, conn);
                    cmd.Parameters.AddWithValue("@StartTime", startTime);
                    cmd.Parameters.AddWithValue("@EndTime", endTime);
                    if (!string.IsNullOrEmpty(factoryCode))
                    {
                        cmd.Parameters.AddWithValue("@FactoryCode", factoryCode);
                    }

                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            result.Add(new TerminalOperateLog
                            {
                                Id = reader.GetInt32(0),
                                TerminalCode = reader.GetString(1),
                                FactoryCode = reader.GetString(2),
                                LineCode = reader.GetString(3),
                                MaterialCode = reader.GetString(4),
                                OperationType = (MaterialOperationEnum)reader.GetInt32(5),
                                OperateQty = (decimal)reader.GetDouble(6),
                                OrderNo = reader.GetString(7),
                                Operator = reader.GetString(8),
                                OperateTime = reader.GetDateTime(9),
                                Status = (TerminalOperateStatusEnum)reader.GetInt32(10),
                                ErrorMsg = reader.GetString(11)
                            });
                        }
                    }
                }
            });
            return result;
        }

        /// <summary>
        /// 获取库存预警物料
        /// </summary>
        public async Task<List<MaterialQueryResponseDto>> GetStockWarningMaterialsAsync(string factoryCode)
        {
            // 1. 从ERP获取工厂下所有物料
            // 注:实际项目中需调用SAP批量查询RFC,此处简化为模拟数据
            var warningMaterials = new List<MaterialQueryResponseDto>();
            
            // 模拟:查询ERP物料,筛选库存低于安全库存的物料
            var testMaterialCodes = new List<string> { "MAT001", "MAT002", "MAT003" };
            foreach (var code in testMaterialCodes)
            {
                var material = await _erpMaterialService.QueryErpMaterialAsync(code, factoryCode, string.Empty);
                if (material.StockQty < material.SafeStockQty)
                {
                    warningMaterials.Add(material);
                }
            }
            return warningMaterials;
        }
    }
}
7. API 控制器实现

csharp

运行

// Controllers/MaterialControlController.cs
using ErpMaterialMiddleware.Models.Dto;
using ErpMaterialMiddleware.Services;
using Microsoft.AspNetCore.Mvc;

namespace ErpMaterialMiddleware.Controllers
{
    /// <summary>
    /// 多控制端物料接口
    /// </summary>
    [Route("api/control/[controller]")]
    [ApiController]
    public class MaterialController : ControllerBase
    {
        private readonly IMaterialControlService _materialControlService;

        public MaterialController(IMaterialControlService materialControlService)
        {
            _materialControlService = materialControlService;
        }

        /// <summary>
        /// 物料查询(多控制端)
        /// </summary>
        [HttpPost("query")]
        public async Task<IActionResult> QueryMaterial([FromBody] MaterialQueryDto dto)
        {
            try
            {
                var result = await _materialControlService.QueryMaterialAsync(dto);
                return Ok(new { Code = 200, Message = "查询成功", Data = result });
            }
            catch (Exception ex)
            {
                return BadRequest(new { Code = 500, Message = ex.Message });
            }
        }

        /// <summary>
        /// 物料领用(多控制端)
        /// </summary>
        [HttpPost("take")]
        public async Task<IActionResult> TakeMaterial([FromBody] MaterialTakeDto dto)
        {
            string errorMsg;
            var success = await _materialControlService.TakeMaterialAsync(dto, out errorMsg);
            if (success)
            {
                return Ok(new { Code = 200, Message = string.IsNullOrEmpty(errorMsg) ? "领用成功" : errorMsg });
            }
            else
            {
                return BadRequest(new { Code = 500, Message = errorMsg });
            }
        }

        /// <summary>
        /// 手动触发离线数据补发
        /// </summary>
        [HttpPost("sync-offline")]
        public async Task<IActionResult> SyncOfflineData()
        {
            await _materialControlService.SyncOfflineDataAsync();
            return Ok(new { Code = 200, Message = "离线数据补发任务已执行" });
        }
    }
}

// Controllers/MaterialConsoleController.cs
using ErpMaterialMiddleware.Models.Dto;
using ErpMaterialMiddleware.Services;
using Microsoft.AspNetCore.Mvc;

namespace ErpMaterialMiddleware.Controllers
{
    /// <summary>
    /// 汇总控制台物料接口
    /// </summary>
    [Route("api/console/[controller]")]
    [ApiController]
    public class MaterialController : ControllerBase
    {
        private readonly IMaterialConsoleService _materialConsoleService;

        public MaterialController(IMaterialConsoleService materialConsoleService)
        {
            _materialConsoleService = materialConsoleService;
        }

        /// <summary>
        /// 获取物料操作汇总数据
        /// </summary>
        [HttpPost("summary")]
        public async Task<IActionResult> GetMaterialSummary([FromBody] MaterialSummaryQueryDto dto)
        {
            try
            {
                var result = await _materialConsoleService.GetMaterialSummaryAsync(dto);
                return Ok(new { Code = 200, Message = "汇总查询成功", Data = result });
            }
            catch (Exception ex)
            {
                return BadRequest(new { Code = 500, Message = ex.Message });
            }
        }

        /// <summary>
        /// 获取终端操作日志
        /// </summary>
        [HttpPost("operate-log")]
        public async Task<IActionResult> GetOperateLog(string factoryCode, DateTime startTime, DateTime endTime, string adminToken)
        {
            try
            {
                // 简化认证:实际需放到DTO中
                var consoleAuth = new Utils.ConsoleAuthHelper();
                if (!consoleAuth.ValidateConsoleToken(adminToken))
                {
                    return Forbid("控制台认证失败");
                }
                var result = await _materialConsoleService.GetTerminalOperateLogAsync(factoryCode, startTime, endTime);
                return Ok(new { Code = 200, Message = "日志查询成功", Data = result });
            }
            catch (Exception ex)
            {
                return BadRequest(new { Code = 500, Message = ex.Message });
            }
        }

        /// <summary>
        /// 获取库存预警物料
        /// </summary>
        [HttpPost("stock-warning")]
        public async Task<IActionResult> GetStockWarning(string factoryCode, string adminToken)
        {
            try
            {
                var consoleAuth = new Utils.ConsoleAuthHelper();
                if (!consoleAuth.ValidateConsoleToken(adminToken))
                {
                    return Forbid("控制台认证失败");
                }
                var result = await _materialConsoleService.GetStockWarningMaterialsAsync(factoryCode);
                return Ok(new { Code = 200, Message = "预警查询成功", Data = result });
            }
            catch (Exception ex)
            {
                return BadRequest(new { Code = 500, Message = ex.Message });
            }
        }
    }
}
8. 项目启动配置(Program.cs)

csharp

运行

using ErpMaterialMiddleware.Services;
using ErpMaterialMiddleware.Utils;

var builder = WebApplication.CreateBuilder(args);

// 添加控制器
builder.Services.AddControllers();

// 添加Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "ERP物料中间件API", Version = "v1" });
});

// 注册服务
builder.Services.AddScoped<IErpMaterialService, ErpMaterialService>();
builder.Services.AddScoped<IMaterialControlService, MaterialControlService>();
builder.Services.AddScoped<IMaterialConsoleService, MaterialConsoleService>();
builder.Services.AddSingleton<TerminalAuthHelper>();
builder.Services.AddSingleton<ConsoleAuthHelper>();
builder.Services.AddSingleton<OfflineDataHelper>();
builder.Services.AddHttpClient();

// 添加全局异常处理
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ExceptionHandler>();
});

var app = builder.Build();

// 开发环境启用Swagger
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ERP物料中间件API v1"));
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

// 启动离线数据补发后台任务(定时执行,此处简化为启动时执行一次)
using (var scope = app.Services.CreateScope())
{
    var controlService = scope.ServiceProvider.GetRequiredService<IMaterialControlService>();
    _ = controlService.SyncOfflineDataAsync();
}

app.Run();

三、前置条件与部署说明

  1. 环境依赖

    • .NET 8 SDK;
    • SAP NCO 3.1+ NuGet 包(Install-Package SAPNCO);
    • SQLite(本地存储)或 SQL Server(生产环境);
    • 提前在 SAP 中创建自定义 RFC:Z_MATERIAL_QUERY(物料查询)、Z_MATERIAL_TAKE(物料领用)、Z_MATERIAL_RETURN(物料退料)。
  2. 接口测试示例

    • 控制端物料查询请求:

      json

      {
        "TerminalCode": "TERM001",
        "AuthKey": "TERM001_2026_KEY",
        "MaterialCode": "MAT001",
        "FactoryCode": "F01",
        "LineCode": "L01"
      }
      
    • 控制台汇总查询请求:

      json

      {
        "AdminToken": "CONSOLE_ADMIN_2026_TOKEN",
        "FactoryCode": "F01",
        "StartTime": "2026-01-01",
        "EndTime": "2026-01-05"
      }
      

总结

  1. 核心设计亮点

    • 适配 “多控制端 + 汇总控制台” 模式:控制端提供细粒度物料操作 API,控制台提供汇总 / 审计 / 预警 API;
    • 断网容错:离线缓存 + 自动补发,保证终端操作不丢失;
    • 权限隔离:按终端维度控制操作权限,按工厂维度隔离数据;
    • 数据一致性:所有操作最终同步到 ERP,控制台数据基于 ERP + 本地日志汇总。
  2. 生产环境扩展建议

    • 增加定时任务:使用Quartz.NET实现离线数据定时补发(如每 10 分钟);
    • 替换认证方式:终端 / 控制台改用 JWT/OAuth2.0 认证,避免硬编码密钥;
    • 增加监控告警:控制台添加物料预警推送(邮件 / 短信);
    • 性能优化:对 ERP 物料数据做本地缓存,减少 SAP RFC 调用压力;
    • 日志审计:对接 ELK 栈,实现操作日志的可视化分析。

该中间件完全基于.NET 8 架构实现,可直接替换 SAP 连接参数、终端配置后部署,满足制造业多终端物料操作 + 控制台统一管控的核心诉求。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐