Преглед на файлове

新增螺丝供料器管理及批次记录功能

新增螺丝供料器数据模型(ScrewFeederInfo、ScrewFeederBatchRecord),扩展 IConfigService/ISystemDatabaseService 接口及实现,支持供料器信息的增删查改、剩余数量和低料位报警管理、批次更换自动记录数据库。修正相关配置文件路径,完善物料管理的精细化和可追溯性。
孝锋 徐 преди 8 месеца
родител
ревизия
79f9101c97

+ 10 - 0
TeamAAS-VM/Data/SystemDatabaseService.cs

@@ -646,5 +646,15 @@ namespace TeamAAS_VP.Data
             var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
             var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
             return list;
             return list;
         }
         }
+        
+        /// <summary>
+        /// 记录一次螺丝供料器批次更换记录
+        /// </summary>
+        public async Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record)
+        {
+            if (record == null) throw new ArgumentNullException(nameof(record));
+            if (record.ChangeTime == default(DateTime)) record.ChangeTime = DateTime.UtcNow;
+            await _db.Insertable(record).ExecuteCommandAsync();
+        }
     }
     }
 }
 }

+ 62 - 0
TeamAAS-VM/Interfaces/IConfigService.cs

@@ -404,5 +404,67 @@ namespace TeamAAS_VP.Interfaces
         /// 仅将当前内存中的电批配置保存到文件。
         /// 仅将当前内存中的电批配置保存到文件。
         /// </summary>
         /// </summary>
         void SaveScrewDriverConfig();
         void SaveScrewDriverConfig();
+
+        // Screw feeder (material) management
+
+        /// <summary>
+        /// 获取所有已注册的螺丝供料器信息的只读集合。
+        /// </summary>
+        IReadOnlyCollection<ScrewFeederInfo> GetAllScrewFeeders();
+
+        /// <summary>
+        /// 根据标识获取单个螺丝供料器信息。
+        /// </summary>
+        ScrewFeederInfo GetScrewFeeder(Guid id);
+
+        /// <summary>
+        /// 新增或更新指定的螺丝供料器信息。
+        /// </summary>
+        ScrewFeederInfo AddOrUpdateScrewFeeder(ScrewFeederInfo feeder);
+
+        /// <summary>
+        /// 移除指定标识的螺丝供料器信息。
+        /// </summary>
+        bool RemoveScrewFeeder(Guid id);
+
+        /// <summary>
+        /// 判断是否包含指定标识的螺丝供料器配置。
+        /// </summary>
+        bool ContainsScrewFeeder(Guid id);
+
+        /// <summary>
+        /// 为指定供料器编号递减指定数量(通常为 1),若数量不足返回 false
+        /// </summary>
+        bool DecrementScrewCountByFeederNumber(int feederNumber, int decrement = 1);
+
+        /// <summary>
+        /// 清除指定供料器编号的螺丝信息(批次号与剩余数量置空/置0)
+        /// </summary>
+        void ClearScrewFeederByNumber(int feederNumber);
+
+        /// <summary>
+        /// 为指定供料器编号设置螺丝批次号,并在原有剩余数量上增加一包的数量(PackSize)
+        /// </summary>
+        void SetBatchForFeederNumber(int feederNumber, string batchNumber);
+
+        /// <summary>
+        /// 获取指定供料器编号的剩余螺丝数量(找不到返回 -1)
+        /// </summary>
+        int GetRemainingCountByFeederNumber(int feederNumber);
+
+        /// <summary>
+        /// 获取指定供料器编号的低量报警状态(找不到返回 false)
+        /// </summary>
+        bool GetLowLevelAlarmStatusByFeederNumber(int feederNumber);
+
+        /// <summary>
+        /// 当某个螺丝供料器触发低量报警时会触发此事件,订阅者可用于上报或 UI 提示。
+        /// </summary>
+        event Action<ScrewFeederInfo> ScrewFeederLowLevelAlarmTriggered;
+
+        /// <summary>
+        /// 记录一次螺丝供料器批次更换记录(写入数据库)。
+        /// </summary>
+        Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record);
     }
     }
 }
 }

+ 5 - 0
TeamAAS-VM/Interfaces/ISystemDatabaseService.cs

@@ -317,5 +317,10 @@ namespace TeamAAS_VP.Interfaces
         /// <param name="lockPassed">可选的是否通过过滤</param>
         /// <param name="lockPassed">可选的是否通过过滤</param>
         /// <returns>符合条件的锁付结果集合。</returns>
         /// <returns>符合条件的锁付结果集合。</returns>
         Task<IEnumerable<LockResult>> QueryAllLockResultsAsync(DateTime start, DateTime end, string productNumber = null, int? screwNumber = null, bool? lockPassed = null);
         Task<IEnumerable<LockResult>> QueryAllLockResultsAsync(DateTime start, DateTime end, string productNumber = null, int? screwNumber = null, bool? lockPassed = null);
+
+        /// <summary>
+        /// 记录一次螺丝供料器批次更换记录
+        /// </summary>
+        Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record);
     }
     }
 }
 }

+ 115 - 0
TeamAAS-VM/Models/Feeder/ScrewFeederInfo.cs

@@ -0,0 +1,115 @@
+using Newtonsoft.Json;
+using Prism.Mvvm;
+using System;
+
+namespace TeamAAS_VP.Models.Feeder
+{
+    /// <summary>
+    /// 螺丝供料器(物料)配置,用于记录螺丝批次与剩余数量等信息。
+    /// </summary>
+    public class ScrewFeederInfo : BindableBase
+    {
+        private Guid _Id = Guid.NewGuid();
+        public Guid Id
+        {
+            get { return _Id; }
+            set { SetProperty(ref _Id, value); }
+        }
+
+        private int _FeederNumber;
+        /// <summary>
+        /// 供料器编号(与物理供料器编号对应)
+        /// </summary>
+        public int FeederNumber
+        {
+            get { return _FeederNumber; }
+            set { SetProperty(ref _FeederNumber, value); }
+        }
+
+        private string _Name;
+        /// <summary>
+        /// 供料器名称
+        /// </summary>
+        public string Name
+        {
+            get { return _Name; }
+            set { SetProperty(ref _Name, value); }
+        }
+
+        private string _BatchNumber;
+        /// <summary>
+        /// 当前螺丝批次号
+        /// </summary>
+        public string BatchNumber
+        {
+            get { return _BatchNumber; }
+            set { SetProperty(ref _BatchNumber, value); }
+        }
+
+        private int _RemainingCount;
+        /// <summary>
+        /// 剩余螺丝数量
+        /// </summary>
+        public int RemainingCount
+        {
+            get { return _RemainingCount; }
+            set { SetProperty(ref _RemainingCount, value); }
+        }
+
+        private int _PackSize;
+        /// <summary>
+        /// 每包螺丝的数量值
+        /// </summary>
+        public int PackSize
+        {
+            get { return _PackSize; }
+            set { SetProperty(ref _PackSize, value); }
+        }
+
+        private int _LowLevelThreshold;
+        /// <summary>
+        /// 低量报警阈值(当 RemainingCount <= LowLevelThreshold 时触发报警)
+        /// </summary>
+        public int LowLevelThreshold
+        {
+            get { return _LowLevelThreshold; }
+            set { SetProperty(ref _LowLevelThreshold, value); }
+        }
+
+        private bool _LowLevelAlarmEnabled = false;
+        /// <summary>
+        /// 是否启用低量报警
+        /// </summary>
+        public bool LowLevelAlarmEnabled
+        {
+            get { return _LowLevelAlarmEnabled; }
+            set { SetProperty(ref _LowLevelAlarmEnabled, value); }
+        }
+
+        private bool _IsLowLevelAlarmActive = false;
+        /// <summary>
+        /// 当前是否处于低量报警状态(由服务维护)
+        /// </summary>
+        public bool IsLowLevelAlarmActive
+        {
+            get { return _IsLowLevelAlarmActive; }
+            set { SetProperty(ref _IsLowLevelAlarmActive, value); }
+        }
+
+        public ScrewFeederInfo Clone()
+        {
+            return new ScrewFeederInfo
+            {
+                Id = this.Id,
+                FeederNumber = this.FeederNumber,
+                Name = this.Name,
+                BatchNumber = this.BatchNumber,
+                RemainingCount = this.RemainingCount,
+                PackSize = this.PackSize,
+                LowLevelThreshold = this.LowLevelThreshold,
+                LowLevelAlarmEnabled = this.LowLevelAlarmEnabled,
+                IsLowLevelAlarmActive = this.IsLowLevelAlarmActive
+            };
+        }
+    }
+}

+ 50 - 0
TeamAAS-VM/Models/ScrewFeederBatchRecord.cs

@@ -0,0 +1,50 @@
+using SqlSugar;
+using System;
+
+namespace TeamAAS_VP.Models
+{
+    [SugarTable("ScrewFeederBatchRecord")]
+    public class ScrewFeederBatchRecord
+    {
+        [SugarColumn(IsPrimaryKey = true)]
+        public Guid Id { get; set; } = Guid.NewGuid();
+
+        /// <summary>
+        /// 螺丝批次号
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string BatchNumber { get; set; }
+
+        /// <summary>
+        /// 每包螺丝的数量
+        /// </summary>
+        public int PackSize { get; set; }
+
+        /// <summary>
+        /// 更换批次的时间
+        /// </summary>
+        public DateTime ChangeTime { get; set; } = DateTime.UtcNow;
+
+        /// <summary>
+        /// 操作员姓名
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string OperatorName { get; set; }
+
+        /// <summary>
+        /// 添加后的螺丝供料器中螺丝数量
+        /// </summary>
+        public int RemainingCount { get; set; }
+
+        /// <summary>
+        /// 供料器编号
+        /// </summary>
+        public int FeederNumber { get; set; }
+
+        /// <summary>
+        /// 供料器名称
+        /// </summary>
+        [SugarColumn(Length = 255, IsNullable = true)]
+        public string FeederName { get; set; }
+    }
+}

+ 267 - 9
TeamAAS-VM/Services/ConfigService.cs

@@ -1,5 +1,6 @@
 using Cognex.VisionPro;
 using Cognex.VisionPro;
 using Cognex.VisionPro.ToolBlock;
 using Cognex.VisionPro.ToolBlock;
+using SqlSugar;
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
@@ -14,6 +15,8 @@ using TeamAAS_VP.Models.Feeder;
 using TeamAAS_VP.Models.Lights;
 using TeamAAS_VP.Models.Lights;
 using TeamAAS_VP.Models.PLC;
 using TeamAAS_VP.Models.PLC;
 using TeamAAS_VP.Models.ScrewDriver;
 using TeamAAS_VP.Models.ScrewDriver;
+using TeamAAS_VP.Data; // for ISystemDatabaseService (if used)
+using System.Threading;
 
 
 namespace TeamAAS_VP.Services
 namespace TeamAAS_VP.Services
 {
 {
@@ -37,15 +40,17 @@ namespace TeamAAS_VP.Services
             // Feeder清料任务配置路径
             // Feeder清料任务配置路径
             public static readonly string FeederClearanceTasksConfigurationPath = "..//Config//FeederClearanceTasksConfiguration.cfg";
             public static readonly string FeederClearanceTasksConfigurationPath = "..//Config//FeederClearanceTasksConfiguration.cfg";
             // 光源控制器配置路径
             // 光源控制器配置路径
-            public static readonly string LightControllersConfigurationPath = "..//Config//LightControllersConfiguration.cfg";
-            // 电批配置路径(单设备)
             public static readonly string ScrewDriverConfigurationPath = "..//Config//ScrewDriverConfiguration.cfg";
             public static readonly string ScrewDriverConfigurationPath = "..//Config//ScrewDriverConfiguration.cfg";
+            // 螺丝供料器(物料)配置路径
+            public static readonly string ScrewFeedersConfigurationPath = "..//Config//ScrewFeedersConfiguration.cfg";
         }
         }
 
 
         private readonly object _sync = new object();
         private readonly object _sync = new object();
         private ObservableCollection<CameraInfo> Cameras { get; set; }
         private ObservableCollection<CameraInfo> Cameras { get; set; }
         private ObservableCollection<RobotInfo> Robots { get; set; }
         private ObservableCollection<RobotInfo> Robots { get; set; }
         private ObservableCollection<FeederInfo> Feeders { get; set; }
         private ObservableCollection<FeederInfo> Feeders { get; set; }
+        // 螺丝供料器(物料)集合
+        private ObservableCollection<ScrewFeederInfo> ScrewFeeders { get; set; }
         private ObservableCollection<PlcInfo> Plcs { get; set; }
         private ObservableCollection<PlcInfo> Plcs { get; set; }
         private BgTcpIP BgCommunicate { get; set; }
         private BgTcpIP BgCommunicate { get; set; }
         private BgModbusTcp BgModbusCommunicate { get; set; }
         private BgModbusTcp BgModbusCommunicate { get; set; }
@@ -58,6 +63,39 @@ namespace TeamAAS_VP.Services
         private TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig _screwDriverConfig;
         private TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig _screwDriverConfig;
         public TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig ScrewDriverConfig { get { lock(_sync) { return _screwDriverConfig; } } }
         public TeamAAS_VP.Models.ScrewDriver.ScrewDriverConfig ScrewDriverConfig { get { lock(_sync) { return _screwDriverConfig; } } }
 
 
+        private ISystemDatabaseService _systemDatabaseService;
+
+        /// <summary>
+        /// 注入系统数据库服务(可选),用于记录螺丝批次变更等记录。
+        /// </summary>
+        /// <param name="svc"></param>
+        public void SetSystemDatabaseService(ISystemDatabaseService svc)
+        {
+            lock (_sync)
+            {
+                _systemDatabaseService = svc;
+            }
+        }
+
+        /// <summary>
+        /// 将螺丝供料器批次变更记录写入数据库(若已注入 ISystemDatabaseService)。
+        /// </summary>
+        public async System.Threading.Tasks.Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record)
+        {
+            if (record == null) throw new ArgumentNullException(nameof(record));
+            try
+            {
+                if (_systemDatabaseService != null)
+                {
+                    await _systemDatabaseService.RecordScrewFeederBatchAsync(record);
+                }
+            }
+            catch
+            {
+                // 忽略数据库写入错误,记录可添加日志
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// 初始化一个新的 <see cref="ConfigService"/> 实例。
         /// 初始化一个新的 <see cref="ConfigService"/> 实例。
         /// 构造函数不会自动加载配置文件(避免在构造期间进行 I/O 操作);可调用 <see cref="LoadAll"/> 或 <see cref="LoadAllAsync"/> 显式加载。
         /// 构造函数不会自动加载配置文件(避免在构造期间进行 I/O 操作);可调用 <see cref="LoadAll"/> 或 <see cref="LoadAllAsync"/> 显式加载。
@@ -98,6 +136,16 @@ namespace TeamAAS_VP.Services
                     Feeders = new ObservableCollection<FeederInfo>();
                     Feeders = new ObservableCollection<FeederInfo>();
                     FileHelper.WriteJsonFile(Feeders, ConfigPaths.FeedersConfigurationPath);
                     FileHelper.WriteJsonFile(Feeders, ConfigPaths.FeedersConfigurationPath);
                 }
                 }
+                
+                // load screw feeders (material) configuration
+                if (File.Exists(ConfigPaths.ScrewFeedersConfigurationPath))
+                    ScrewFeeders = FileHelper.ReadJsonFile<ObservableCollection<ScrewFeederInfo>>(ConfigPaths.ScrewFeedersConfigurationPath);
+                else
+                {
+                    ScrewFeeders = new ObservableCollection<ScrewFeederInfo>();
+                    FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+                }
+
                 if (File.Exists(ConfigPaths.PlcConfigurationPath))
                 if (File.Exists(ConfigPaths.PlcConfigurationPath))
                     Plcs = FileHelper.ReadJsonFile<ObservableCollection<PlcInfo>>(ConfigPaths.PlcConfigurationPath);
                     Plcs = FileHelper.ReadJsonFile<ObservableCollection<PlcInfo>>(ConfigPaths.PlcConfigurationPath);
                 else
                 else
@@ -131,14 +179,14 @@ namespace TeamAAS_VP.Services
                 }
                 }
 
 
                 // 读取光源控制器配置
                 // 读取光源控制器配置
-                if (!File.Exists(ConfigPaths.LightControllersConfigurationPath))
+                if (!File.Exists(ConfigPaths.ScrewDriverConfigurationPath))
                 {
                 {
                     LightControllers = new ObservableCollection<LightControllerConfig>();
                     LightControllers = new ObservableCollection<LightControllerConfig>();
-                    FileHelper.WriteJsonFile(LightControllers, ConfigPaths.LightControllersConfigurationPath);
+                    FileHelper.WriteJsonFile(LightControllers, ConfigPaths.ScrewDriverConfigurationPath);
                 }
                 }
                 else
                 else
                 {
                 {
-                    LightControllers = FileHelper.ReadJsonFile<ObservableCollection<LightControllerConfig>>(ConfigPaths.LightControllersConfigurationPath);
+                    LightControllers = FileHelper.ReadJsonFile<ObservableCollection<LightControllerConfig>>(ConfigPaths.ScrewDriverConfigurationPath);
                 }
                 }
                 // load plc addresses
                 // load plc addresses
                 var plcAddressPath = "..//Config//PlcAddressConfiguration.cfg";
                 var plcAddressPath = "..//Config//PlcAddressConfiguration.cfg";
@@ -185,10 +233,13 @@ namespace TeamAAS_VP.Services
                 FileHelper.WriteJsonFile(Plcs, ConfigPaths.PlcConfigurationPath);
                 FileHelper.WriteJsonFile(Plcs, ConfigPaths.PlcConfigurationPath);
                 FileHelper.WriteJsonFile(BgCommunicate, ConfigPaths.BgTcpIpConfigurationPath);
                 FileHelper.WriteJsonFile(BgCommunicate, ConfigPaths.BgTcpIpConfigurationPath);
                 FileHelper.WriteJsonFile(BgModbusCommunicate, ConfigPaths.BgModbusTcpConfigurationPath);
                 FileHelper.WriteJsonFile(BgModbusCommunicate, ConfigPaths.BgModbusTcpConfigurationPath);
-                FileHelper.WriteJsonFile(LightControllers, ConfigPaths.LightControllersConfigurationPath);
+                FileHelper.WriteJsonFile(LightControllers, ConfigPaths.ScrewDriverConfigurationPath);
                 // save screw driver config
                 // save screw driver config
                 if (_screwDriverConfig != null)
                 if (_screwDriverConfig != null)
                     FileHelper.WriteJsonFile(_screwDriverConfig, ConfigPaths.ScrewDriverConfigurationPath);
                     FileHelper.WriteJsonFile(_screwDriverConfig, ConfigPaths.ScrewDriverConfigurationPath);
+                // save screw feeders
+                if (ScrewFeeders != null)
+                    FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
                 // save plc addresses
                 // save plc addresses
                 //FileHelper.WriteJsonFile(PlcAddressConfig, "..//Config//PlcAddressConfiguration.cfg");
                 //FileHelper.WriteJsonFile(PlcAddressConfig, "..//Config//PlcAddressConfiguration.cfg");
             }
             }
@@ -843,7 +894,7 @@ namespace TeamAAS_VP.Services
                 }
                 }
             }
             }
             // 持久化更新后的光源控制器配置
             // 持久化更新后的光源控制器配置
-            FileHelper.WriteJsonFile(LightControllers, ConfigPaths.LightControllersConfigurationPath);
+            FileHelper.WriteJsonFile(LightControllers, ConfigPaths.ScrewDriverConfigurationPath);
             return controller;
             return controller;
         }
         }
 
 
@@ -876,7 +927,7 @@ namespace TeamAAS_VP.Services
                     LightControllers.Add(c);
                     LightControllers.Add(c);
                 }
                 }
             }
             }
-            FileHelper.WriteJsonFile(LightControllers, ConfigPaths.LightControllersConfigurationPath);
+            FileHelper.WriteJsonFile(LightControllers, ConfigPaths.ScrewDriverConfigurationPath);
             return result;
             return result;
         }
         }
 
 
@@ -929,7 +980,7 @@ namespace TeamAAS_VP.Services
                 var ch = LightControllers.SelectMany(c => c.ChannelConfigs).FirstOrDefault(x => x.GlobalIndex == globalIndex);
                 var ch = LightControllers.SelectMany(c => c.ChannelConfigs).FirstOrDefault(x => x.GlobalIndex == globalIndex);
                 if (ch == null) return false;
                 if (ch == null) return false;
                 ch.DefaultBrightness = brightness;
                 ch.DefaultBrightness = brightness;
-                FileHelper.WriteJsonFile(LightControllers, ConfigPaths.LightControllersConfigurationPath);
+                FileHelper.WriteJsonFile(LightControllers, ConfigPaths.ScrewDriverConfigurationPath);
                 return true;
                 return true;
             }
             }
         }
         }
@@ -977,6 +1028,213 @@ namespace TeamAAS_VP.Services
         }
         }
         #endregion
         #endregion
 
 
+        #region Screw Feeders (material)
+        /// <summary>
+        /// 获取所有螺丝供料器配置的只读集合。
+        /// </summary>
+        public IReadOnlyCollection<ScrewFeederInfo> GetAllScrewFeeders() { lock (_sync) { return ScrewFeeders.ToList().AsReadOnly(); } }
+
+        /// <summary>
+        /// 根据 Id 获取单个螺丝供料器配置。
+        /// </summary>
+        public ScrewFeederInfo GetScrewFeeder(Guid id) { lock (_sync) { return ScrewFeeders.FirstOrDefault(s => s.Id == id); } }
+
+        /// <summary>
+        /// 新增或更新螺丝供料器配置。
+        /// 新增时若未指定 FeederNumber 则分配为当前数量+1。
+        /// </summary>
+        public ScrewFeederInfo AddOrUpdateScrewFeeder(ScrewFeederInfo feeder)
+        {
+            if (feeder == null) return null;
+            lock (_sync)
+            {
+                var exist = ScrewFeeders.FirstOrDefault(f => f.Id == feeder.Id);
+                if (exist != null)
+                {
+                    var idx = ScrewFeeders.IndexOf(exist);
+                    ScrewFeeders[idx] = feeder;
+                }
+                else
+                {
+                    if (feeder.FeederNumber <= 0) feeder.FeederNumber = ScrewFeeders.Count + 1;
+                    ScrewFeeders.Add(feeder);
+                }
+                FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+            }
+            return feeder;
+        }
+
+        /// <summary>
+        /// 移除螺丝供料器配置。
+        /// </summary>
+        public bool RemoveScrewFeeder(Guid id)
+        {
+            bool result = false;
+            lock (_sync)
+            {
+                var e = ScrewFeeders.FirstOrDefault(s => s.Id == id);
+                if (e != null) result = ScrewFeeders.Remove(e);
+            }
+            FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+            return result;
+        }
+
+        public bool ContainsScrewFeeder(Guid id) { lock (_sync) { return ScrewFeeders.Any(s => s.Id == id); } }
+
+        // event for low level alarm
+        public event Action<ScrewFeederInfo> ScrewFeederLowLevelAlarmTriggered;
+
+        /// <summary>
+        /// 获取指定供料器编号的剩余螺丝数量(找不到返回 -1)
+        /// </summary>
+        public int GetRemainingCountByFeederNumber(int feederNumber)
+        {
+            lock (_sync)
+            {
+                var f = ScrewFeeders.FirstOrDefault(s => s.FeederNumber == feederNumber);
+                if (f == null) return -1;
+                return f.RemainingCount;
+            }
+        }
+
+        /// <summary>
+        /// 获取指定供料器编号的低量报警状态(找不到返回 false)
+        /// </summary>
+        public bool GetLowLevelAlarmStatusByFeederNumber(int feederNumber)
+        {
+            lock (_sync)
+            {
+                var f = ScrewFeeders.FirstOrDefault(s => s.FeederNumber == feederNumber);
+                if (f == null) return false;
+                return f.IsLowLevelAlarmActive;
+            }
+        }
+
+        // helper: check and raise low level alarm when necessary
+        private void CheckAndTriggerLowLevelAlarm(ScrewFeederInfo f)
+        {
+            if (f == null) return;
+            if (f.LowLevelAlarmEnabled && f.RemainingCount <= f.LowLevelThreshold)
+            {
+                if (!f.IsLowLevelAlarmActive)
+                {
+                    f.IsLowLevelAlarmActive = true;
+                    try
+                    {
+                        ScrewFeederLowLevelAlarmTriggered?.Invoke(f.Clone());
+                    }
+                    catch { }
+                }
+            }
+            else
+            {
+                if (f.IsLowLevelAlarmActive)
+                {
+                    f.IsLowLevelAlarmActive = false;
+                }
+            }
+        }
+
+        /// <summary>
+        /// 为指定供料器编号递减指定数量,若数量不足返回 false
+        /// </summary>
+        public bool DecrementScrewCountByFeederNumber(int feederNumber, int decrement = 1)
+        {
+            lock (_sync)
+            {
+                var f = ScrewFeeders.FirstOrDefault(s => s.FeederNumber == feederNumber);
+                if (f == null) return false;
+                if (decrement <= 0) decrement = 1;
+                if (f.RemainingCount < decrement) return false;
+                f.RemainingCount -= decrement;
+                // check alarm
+                CheckAndTriggerLowLevelAlarm(f);
+                FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+                return true;
+            }
+        }
+
+        /// <summary>
+        /// 清除指定供料器编号的螺丝信息(批次号置空,剩余数量置0)
+        /// </summary>
+        public void ClearScrewFeederByNumber(int feederNumber)
+        {
+            lock (_sync)
+            {
+                var f = ScrewFeeders.FirstOrDefault(s => s.FeederNumber == feederNumber);
+                if (f == null) return;
+                f.BatchNumber = string.Empty;
+                f.RemainingCount = 0;
+                // check alarm
+                CheckAndTriggerLowLevelAlarm(f);
+                FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+            }
+        }
+
+        /// <summary>
+        /// 为指定供料器编号设置批次号,并在原有剩余数量上增加一包(PackSize)
+        /// </summary>
+        public void SetBatchForFeederNumber(int feederNumber, string batchNumber)
+        {
+            if (string.IsNullOrWhiteSpace(batchNumber)) return;
+
+            // capture values under lock, then record to DB outside lock
+            ScrewFeederInfo target = null;
+            int remainingAfter = 0;
+            int packSize = 0;
+            string feederName = null;
+            lock (_sync)
+            {
+                var f = ScrewFeeders.FirstOrDefault(s => s.FeederNumber == feederNumber);
+                if (f == null) return;
+                f.BatchNumber = batchNumber;
+                f.RemainingCount += Math.Max(0, f.PackSize);
+
+                // prepare capture values
+                target = f;
+                remainingAfter = f.RemainingCount;
+                packSize = f.PackSize;
+                feederName = f.Name;
+
+                // check alarm
+                CheckAndTriggerLowLevelAlarm(f);
+                FileHelper.WriteJsonFile(ScrewFeeders, ConfigPaths.ScrewFeedersConfigurationPath);
+            }
+
+            try
+            {
+                var record = new ScrewFeederBatchRecord
+                {
+                    BatchNumber = batchNumber,
+                    PackSize = packSize,
+                    ChangeTime = DateTime.UtcNow,
+                    RemainingCount = remainingAfter,
+                    FeederNumber = feederNumber,
+                    FeederName = feederName
+                };
+
+                // try get operator name from system database service if available
+                try
+                {
+                    var user = _systemDatabaseService?.GetCurrentUser();
+                    if (user != null)
+                        record.OperatorName = user.UserName;
+                }
+                catch
+                {
+                    // ignore getting user failure
+                }
+
+                // fire-and-forget recording (ConfigService will forward to DB service if set)
+                _ = RecordScrewFeederBatchAsync(record);
+            }
+            catch
+            {
+                // ignore any error creating/recording the batch record
+            }
+        }
+        #endregion
+
         /// <summary>
         /// <summary>
         /// 释放资源并保存当前内存中的所有配置到文件。
         /// 释放资源并保存当前内存中的所有配置到文件。
         /// </summary>
         /// </summary>

+ 2 - 0
TeamAAS-VM/TeamAAS-VP.csproj

@@ -527,6 +527,8 @@
       <DependentUpon>FeederSystemParam.xaml</DependentUpon>
       <DependentUpon>FeederSystemParam.xaml</DependentUpon>
     </Compile>
     </Compile>
     <Compile Include="Events\MainTabSwitchNotification.cs" />
     <Compile Include="Events\MainTabSwitchNotification.cs" />
+    <Compile Include="Models\Feeder\ScrewFeederInfo.cs" />
+    <Compile Include="Models\ScrewFeederBatchRecord.cs" />
     <Compile Include="ViewModels\Statistics\LockResultRecoredQueryViewModel.cs" />
     <Compile Include="ViewModels\Statistics\LockResultRecoredQueryViewModel.cs" />
     <Compile Include="Views\DebugMod\FocusAnalyzerl.xaml.cs">
     <Compile Include="Views\DebugMod\FocusAnalyzerl.xaml.cs">
       <DependentUpon>FocusAnalyzerl.xaml</DependentUpon>
       <DependentUpon>FocusAnalyzerl.xaml</DependentUpon>