服务端的TigerApi 框架,基于.NET6 2024 版本
Rodney Chen
2024-12-08 006c12987d7dc7c2081fbf6715ebea98b93fdca0
Tiger.Business.MES/Common/WorkBatch.cs
@@ -10,6 +10,9 @@
using Tiger.IBusiness;
using Tiger.Model.Entitys.MES.Position;
using Tiger.Business.MES;
using static Microsoft.CodeAnalysis.CSharp.SyntaxTokenParser;
using Org.BouncyCastle.Ocsp;
using System.IO;
namespace Tiger.Business
{
@@ -47,31 +50,53 @@
        #region Functions
        /// <summary>
        /// 初始化工单资料
        /// 初始化工单批次资料
        /// </summary>
        /// <returns></returns>
        public WorkBatch Init(string lineCode)
        {
            LineCode = lineCode;
            WO = Biz.Db.Queryable<BIZ_MES_WO>().Where(q => q.ORDER_NO == OrderNo).IncludesAllFirstLayer().First();
            Batch = Biz.Db.Queryable<BIZ_MES_WO_BATCH>().Where(q => q.ORDER_NO == OrderNo && q.ACT_LINE == LineCode &&
                                (q.STATUS == BIZ_MES_WO_BATCH.STATUSs.Release.GetValue() || q.STATUS == BIZ_MES_WO_BATCH.STATUSs.Working.GetValue())).First();
            GetBatchInfo();
            return this;
        }
        public void Update()
        /// <summary>
        /// 更新工单批次资料
        /// </summary>
        /// <param name="updateAll"></param>
        public void Update(bool updateAll = false)
        {
            if (!WoContext.WoBatchDic.ContainsKey(Batch.BATCH_NO))
            var strat = DateTime.Now;
            var wo = Biz.Db.Queryable<BIZ_MES_WO>().Where(q => q.ORDER_NO == OrderNo).IncludesAllFirstLayer().First();
            var batch = Biz.Db.Queryable<BIZ_MES_WO_BATCH>().Where(q => q.BATCH_NO == Batch.BATCH_NO).First();
            if (WoContext.WoBatchDic.ContainsKey(batch.BATCH_NO))
            {
                if (wo.STATUS < BIZ_MES_WO.STATUSs.Release.GetValue() || batch.STATUS > BIZ_MES_WO_BATCH.STATUSs.Working.GetValue())
                {
                    WoContext.WoBatchDic.Remove(Batch.BATCH_NO);
                }
                else if(updateAll || wo.CHANGE_TIME > WO.CHANGE_TIME || batch.CHANGE_TIME > Batch.CHANGE_TIME)
                {
                    GetBatchInfo();
                }
            }
            else
            {
                GetBatchInfo();
                WoContext.WoBatchDic.Add(Batch.BATCH_NO, this);
                WoContext.WoBatchDic.Add(batch.BATCH_NO, this);
            }
            WO = wo;
            Batch = batch;
            ConsoleExt.WriteLine($"{DateTime.Now:HH:mm:ss} ##### : {(DateTime.Now - strat).TotalSeconds}s", ConsoleColor.Yellow);
        }
        private void GetBatchInfo()
        {
            WO = Biz.Db.Queryable<BIZ_MES_WO>().Where(q => q.ORDER_NO == OrderNo).IncludesAllFirstLayer().First();
            ConsoleExt.WriteLine($"{DateTime.Now:HH:mm:ss} !!!!! ", ConsoleColor.Cyan);
            Product = Biz.Db.Queryable<BAS_ITEM>().Where(q => q.ITEM_CODE == WO.ITEM_CODE && q.AUTH_ORG == WO.AUTH_ORG).IncludesAllFirstLayer().First();
            Batch = Biz.Db.Queryable<BIZ_MES_WO_BATCH>().Where(q => q.ORDER_NO == OrderNo && q.ACT_LINE == LineCode &&
                                (q.STATUS == BIZ_MES_WO_BATCH.STATUSs.Release.GetValue() || q.STATUS == BIZ_MES_WO_BATCH.STATUSs.Working.GetValue())).First();
            Customer = Biz.Db.Queryable<BAS_CUSTOMER>().Where(q => q.CUST_CODE == WO.CUST_CODE).First();
            WoSNs = Biz.Db.Queryable<BIZ_MES_WO_SN>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            Edges = Biz.Db.Queryable<MES_WO_EDGE>().Where(q => q.WORK_ORDER == OrderNo).ToList();
@@ -90,7 +115,7 @@
        }
        /// <summary>
        /// 添加节点的下一个行为到工步列表
        /// 递归遍历工序节点,给下级工序节点排序和整理前置工序
        /// </summary>
        /// <param name="parent"></param>
        private void NodeSorting(MES_WO_NODE parent)
@@ -126,15 +151,15 @@
        {
            var action = new ApiAction<SubmitOutput>(new SubmitOutput(), true);
            WO = Biz.Db.Queryable<BIZ_MES_WO>().Where(q => q.ORDER_NO == OrderNo).IncludesAllFirstLayer().First();
            Batch = Biz.Db.Queryable<BIZ_MES_WO_BATCH>().Where(q => q.BATCH_NO == Batch.BATCH_NO).First();
            Update();
            //工单批次状态不是已下发或者生产中,则不允许生产
            if (Batch.STATUS != BIZ_MES_WO_BATCH.STATUSs.Release.GetValue() && Batch.STATUS != BIZ_MES_WO_BATCH.STATUSs.Working.GetValue())
            {
                action.IsSuccessed = false;
                action.Data.SetValue(this, null);
                //action.LocaleMsg = new($"工单批次[{Batch.BATCH_NO}]状态[{Batch.STATUS.GetEnum<BIZ_MES_WO_BATCH.STATUSs>().GetName()}]不是允许生产的状态,请扫描允许生产的产品条码");
                action.LocaleMsg = new("MES.WorkBatch.WoBatchStatusCanNotWork", Batch.BATCH_NO, Batch.STATUS.GetEnum<BIZ_MES_WO_BATCH.STATUSs>().GetName());
                action.LocaleMsg = new("MES.WorkBatch.WoBatchStatusCanNotWork", Batch.BATCH_NO, Batch.STATUS.GetEnumDesc<BIZ_MES_WO_BATCH.STATUSs>());
            }
            //工单状态不是已下发或者生产中,则不允许生产
            if (WO.STATUS != BIZ_MES_WO.STATUSs.Release.GetValue() && WO.STATUS != BIZ_MES_WO.STATUSs.Working.GetValue())
@@ -142,24 +167,25 @@
                action.IsSuccessed = false;
                action.Data.SetValue(this, null);
                //action.LocaleMsg = new($"工单[{WO.ORDER_NO}]状态[{Batch.STATUS.GetEnum<BIZ_MES_WO.STATUSs>().GetName()}]不是允许生产的状态,请扫描允许生产的产品条码");
                action.LocaleMsg = new("MES.WorkBatch.WoStatusCanNotWork", WO.ORDER_NO, Batch.STATUS.GetEnum<BIZ_MES_WO.STATUSs>().GetName());
                action.LocaleMsg = new("MES.WorkBatch.WoStatusCanNotWork", WO.ORDER_NO, Batch.STATUS.GetEnumDesc<BIZ_MES_WO.STATUSs>());
            }
            //工单批次投入数量减去报废数量如果大于等于计划数量,则不允许生产
            //if (curNode.IS_INPUT == "Y" && Batch.INPUT_QTY - Batch.SCRAP_QTY >= Batch.PLAN_QTY)
            if (curNodeSetting.IS_INPUT == "Y" && WoSNs.Count(q => q.STATUS > BIZ_MES_WO_SN.STATUSs.NotInput.GetValue() && q.BATCH_NO == Batch.BATCH_NO) - Batch.SCRAP_QTY >= Batch.PLAN_QTY)
            var batchInput = WoSNs.Where(q => q.BATCH_NO == Batch.BATCH_NO && BIZ_MES_WO_SN.STATUSs.NotInput.GetValue() < q.STATUS && q.STATUS <= BIZ_MES_WO_SN.STATUSs.Finished.GetValue()).DistinctBy(q => q.WIP_ID).Sum(q => q.QTY);
            if (curNode.IS_FIRST_NODE == "Y" && batchInput >= Batch.PLAN_QTY)
            {
                action.IsSuccessed = false;
                action.Data.SetValue(this, null);
                //action.LocaleMsg = new($"工单批次[{0}]已投入 {1},其中报废 {2},以满足计划数量[{3}],无需继续投入");
                action.LocaleMsg = new("MES.WorkBatch.BatchInputEnough", Batch.BATCH_NO, WoSNs.Count(q => q.BATCH_NO == Batch.BATCH_NO), Batch.SCRAP_QTY, Batch.PLAN_QTY);
                action.LocaleMsg = new("MES.WorkBatch.BatchInputEnough", Batch.BATCH_NO, batchInput, WoSNs.Where(q => q.BATCH_NO == Batch.BATCH_NO && q.STATUS > BIZ_MES_WO_SN.STATUSs.Finished.GetValue()).DistinctBy(q => q.WIP_ID).Sum(q => q.QTY), Batch.PLAN_QTY);
            }
            //工单投入数量减去报废数量如果大于等于计划数量,则不允许生产
            if (curNodeSetting.IS_INPUT == "Y" && WoSNs.Count(q => q.STATUS > BIZ_MES_WO_SN.STATUSs.NotInput.GetValue() && q.WORK_ORDER == WO.ORDER_NO) - WO.SCRAP_QTY >= WO.PLAN_QTY)
            var woInput = WoSNs.Where(q => q.WORK_ORDER == WO.ORDER_NO && BIZ_MES_WO_SN.STATUSs.NotInput.GetValue() < q.STATUS && q.STATUS <= BIZ_MES_WO_SN.STATUSs.Finished.GetValue()).DistinctBy(q => q.WIP_ID).Sum(q => q.QTY);
            if (curNode.IS_FIRST_NODE == "Y" && woInput >= WO.PLAN_QTY)
            {
                action.IsSuccessed = false;
                action.Data.SetValue(this, null);
                //action.LocaleMsg = new($"工单[{0}]已投入 {1},其中报废 {2},以满足计划数量[{3}],无需继续投入");
                action.LocaleMsg = new("MES.WorkBatch.WoInputEnough", WO.ORDER_NO, WoSNs.Count(q => q.WORK_ORDER == WO.ORDER_NO), WO.SCRAP_QTY, WO.PLAN_QTY);
                action.LocaleMsg = new("MES.WorkBatch.WoInputEnough", WO.ORDER_NO, woInput, WoSNs.Where(q => q.WORK_ORDER == WO.ORDER_NO && q.STATUS > BIZ_MES_WO_SN.STATUSs.Finished.GetValue()).DistinctBy(q => q.WIP_ID).Sum(q => q.QTY), WO.PLAN_QTY);
            }
            return action;
@@ -182,8 +208,8 @@
                var db = Biz.Db;
                var dbTran = db.UseTran(() =>
                {
                    db.Updateable(WO, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS, q.ACT_START_TIME }).ExecuteCommand();
                    db.Updateable(Batch, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS, q.ACT_START_TIME }).ExecuteCommand();
                    db.Updateable(WO, user).UpdateColumns(q => new { q.STATUS, q.ACT_START_TIME }).ExecuteCommand();
                    db.Updateable(Batch, user).UpdateColumns(q => new { q.STATUS, q.ACT_START_TIME }).ExecuteCommand();
                });
                if (!dbTran.IsSuccess)
                {
@@ -218,6 +244,61 @@
                    return false;
                }
            }
            return true;
        }
        /// <summary>
        /// 检查工单是否完工,已完工则修改相应状态并记录到数据库
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public async Task<bool> CheckIsComplete(string user)
        {
            var woSNs = Biz.Db.Queryable<BIZ_MES_WO_SN>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            //判断当前工单批次是否已完工
            if (!woSNs.Any(q => q.BATCH_NO == Batch.BATCH_NO && q.STATUS < BIZ_MES_WO_SN.STATUSs.Finished.GetValue()) &&
                 woSNs.Count(q => q.BATCH_NO == Batch.BATCH_NO && q.STATUS == BIZ_MES_WO_SN.STATUSs.Finished.GetValue()) == Batch.PLAN_QTY)
            {
                Batch.STATUS = BIZ_MES_WO_BATCH.STATUSs.Closed.GetValue();
                Batch.ACT_END_TIME = DateTime.Now;
            }
            //判断当前工单是否已完工
            if (!woSNs.Any(q => q.STATUS < BIZ_MES_WO_SN.STATUSs.Finished.GetValue()) &&
                 woSNs.Count(q => q.STATUS == BIZ_MES_WO_SN.STATUSs.Finished.GetValue()) == WO.PLAN_QTY)
            {
                WO.STATUS = BIZ_MES_WO.STATUSs.Closed.GetValue();
                WO.ACT_END_TIME = DateTime.Now;
            }
            //保存数据库
            var db = Biz.Db;
            var dbTran = db.UseTran(() =>
            {
                db.Updateable(WO, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS, q.ACT_END_TIME }).ExecuteCommand();
                db.Updateable(Batch, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS, q.ACT_END_TIME }).ExecuteCommand();
            });
            if (!dbTran.IsSuccess)
            {
                //throw dbTran.ErrorException;
                Logger.Default.Fatal(dbTran.ErrorException, $"检查工单批次[{Batch.BATCH_NO}]是否完工异常");
                return false;
            }
            //删除缓存
            if (Batch.STATUS == BIZ_MES_WO_BATCH.STATUSs.Closed.GetValue())
            {
                WoContext.WoBatchDic.Remove(Batch.BATCH_NO);
            }
            if (WO.STATUS == BIZ_MES_WO.STATUSs.Closed.GetValue())
            {
                var list = WoContext.WoBatchDic.Where(q => q.Value.WO.ORDER_NO == OrderNo).Select(q => q.Key);
                foreach (var item in list)
                {
                    WoContext.WoBatchDic.Remove(item);
                }
            }
            return true;
        }
@@ -275,6 +356,7 @@
                    action.IsSuccessed = false;
                    action.Data.SetValue(this, null);
                    var nextList = Nodes.Where(q => q.IS_FIRST_NODE == "Y");
                    //action.LocaleMsg = new($"条码[{0}]在工序[{1}]过站扫描错误,请先通过以下工序:{2}");
                    action.LocaleMsg = new("MES.WorkBatch.GotoNextNodeException", input.SN, nextNode.NODE_NAME, string.Join(", ", nextList.Select(q => q.NODE_NAME + (GetNodeSetting(q)?.CAN_SKIP == "Y" ? $"({Biz.T(Biz.L("MES.WorkBatch.Optional"), input.Locale)})" : ""))));
                    return action;
                }
@@ -311,22 +393,25 @@
                        action.IsSuccessed = false;
                        action.Data.SetValue(this, null);
                        var nextList = reflowNodes.Where(q => wipSN.REFLOW_NODE.IsNullOrEmpty() || wipSN.REFLOW_NODE == q.NODE_NAME);
                        //action.LocaleMsg = new($"工序[{0}]不是条码[{1}]在工序[{2}]维修后可回流的工序,请选择回流到以下工序:{3}");
                        action.LocaleMsg = new("MES.WorkBatch.ReflowToNodeException", nextNode.NODE_NAME, input.SN, curNode.NODE_NAME, string.Join(", ", nextList.Select(q => q.NODE_NAME + (GetNodeSetting(q)?.CAN_SKIP == "Y" ? $"({Biz.T(Biz.L("MES.WorkBatch.Optional"), input.Locale)})" : ""))));
                        return action;
                    }
                }
                //不良品入站:如果产品有不良记录且目标工序不是维修工序且不允许不良品入站,则报错
                else if (wipSN.Defects.Any(q => q.WORK_ORDER == WO.ORDER_NO && q.STATUS < MES_WIP_DFT.STATUSs.Resolved.GetValue())
                     && nextSetting.ALLOW_DFT_IN != "Y" && nextNode.Operation.OPER_TYPE != MES_OPERATION.OPER_TYPEs.Repair.GetValue())
                {
                    action.IsSuccessed = false;
                    action.Data.SetValue(this, null);
                    action.LocaleMsg = new("MES.WorkBatch.PleaseGotoRepair", curNode.NODE_NAME, input.SN);
                    return action;
                }
                //正常工序过站
                else
                {
                    //不良品入站:如果产品有不良记录且目标工序不是维修工序且不允许不良品入站,则报错
                    if (wipSN.Defects.Any(q => q.STATUS < MES_WIP_DFT.STATUSs.Resolved.GetValue())
                     && nextSetting.ALLOW_DFT_IN != "Y" && nextNode.Operation.OPER_TYPE != MES_OPERATION.OPER_TYPEs.Repair.GetValue())
                    {
                        action.IsSuccessed = false;
                        action.Data.SetValue(this, null);
                        //action.LocaleMsg = new($"工序[{0}]不允许不良品入站,条码[{1}]存在不良记录,请先按流程指引操作或者进入维修");
                        action.LocaleMsg = new("MES.WorkBatch.PleaseGotoRepair", curNode.NODE_NAME, input.SN);
                        return action;
                    }
                    //添加条码当前工序的下一个可执行工序
                    var nextNodes = GetNextNodes(curNode, wipSN);
                    //如果下一个可执行工序包含目标工序则允许进站
@@ -337,11 +422,12 @@
                    }
                    //如果当前工序有必须执行的后续工序,则报错
                    if (nextNodes.Any(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y" && s.CAN_SKIP != "Y")))
                    if (nextNodes.Any(q => q.OPER_CODE != "EndNode" && NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y" && s.CAN_SKIP != "Y")))
                    {
                        action.IsSuccessed = false;
                        action.Data.SetValue(this, null);
                        var nextList = nextNodes.Where(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y" && s.CAN_SKIP != "Y"));
                        var nextList = nextNodes.Where(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y"));
                        //action.LocaleMsg = new($"条码[{0}]在工序[{1}]过站扫描错误,请先通过以下工序:{2}");
                        action.LocaleMsg = new("MES.WorkBatch.GotoNextNodeException", input.SN, nextNode.NODE_NAME, string.Join(", ", nextList.Select(q => q.NODE_NAME + (GetNodeSetting(q)?.CAN_SKIP == "Y" ? $"({Biz.T(Biz.L("MES.WorkBatch.Optional"), input.Locale)})" : ""))));
                        return action;
                    }
@@ -350,7 +436,7 @@
                    {
                        //在前置工序查找还有没有后续工序没完成的前置工序
                        var prepIDs = curNode.PrepNodeIDs.Where(id =>
                                                             Edges.Any(e => e.SRC_NODE == id && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.NODE_ID == e.TGT_NODE && h.IsFinished))
                                                             Edges.Any(e => e.SRC_NODE == id && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.UNBIND_FLAG != "Y" && h.NODE_ID == e.TGT_NODE && h.IsFinished))
                                                             ).ToList();
                        foreach (var prepID in prepIDs)
                        {
@@ -371,6 +457,7 @@
                            action.IsSuccessed = false;
                            action.Data.SetValue(this, null);
                            var nextList = nextNodes.Where(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y"));
                            //action.LocaleMsg = new($"条码[{0}]在工序[{1}]过站扫描错误,请先通过以下工序:{2}");
                            action.LocaleMsg = new("MES.WorkBatch.GotoNextNodeException", input.SN, nextNode.NODE_NAME, string.Join(", ", nextList.Select(q => q.NODE_NAME + (GetNodeSetting(q)?.CAN_SKIP == "Y" ? $"({Biz.T(Biz.L("MES.WorkBatch.Optional"), input.Locale)})" : ""))));
                            return action;
                        }
@@ -382,22 +469,22 @@
        }
        /// <summary>
        /// 添加节点的下一个可执行节点
        /// 获取节点的下一个可执行节点(排除有前置工序未完成的节点)
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="wipSN">当前的条码过站记录,需要导航查询生产过程记录信息</param>
        private List<MES_WO_NODE> GetNextNodes(MES_WO_NODE parent, MES_WIP_DATA wipSN)
        {
            var result = new List<MES_WO_NODE>();
            var nextNodes = Nodes.Where(q => q.Operation.OPER_TYPE != MES_OPERATION.OPER_TYPEs.Repair.GetValue()
            var nextNodes =  Nodes.Where(q => q.Operation.OPER_TYPE != MES_OPERATION.OPER_TYPEs.Repair.GetValue()
                                                                && Edges.Any(e => e.SRC_NODE == parent.ID && e.TGT_NODE == q.ID)
                                                                && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.NODE_ID == q.ID && h.IsFinished)
                                                                && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.UNBIND_FLAG != "Y" && h.NODE_ID == q.ID && h.IsFinished)
                                                             ).ToList();
            //尝试将当前工序的后续工序添加到可以执行的工序列表
            foreach (var next in nextNodes)
            {
                //查找有没有前置工序找不到已良品过站的历史记录,若有则不允许继续执行
                if (!next.PrepNodeIDs.Any(id => !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.NODE_ID == id && h.IsFinished)) || parent.IS_FIRST_NODE == "Y")
                if (!next.PrepNodeIDs.Any(id => !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.UNBIND_FLAG != "Y" && h.NODE_ID == id && h.IsFinished)) || parent.IS_FIRST_NODE == "Y")
                {
                    var setting = NodeSets.FirstOrDefault(q => q.NODE_ID == next.ID);
                    //后续工序是启用的,则添加
@@ -417,7 +504,7 @@
        }
        /// <summary>
        /// 根据传入的条码返回下一站可进入进入的目标工序
        /// 根据传入的条码返回下一站可进入的目标工序
        /// </summary>
        /// <param name="wipSN">当前的条码过站记录,需要导航查询生产过程记录和生产不良记录信息</param>
        /// <returns></returns>
@@ -451,7 +538,7 @@
                    {
                        //在前置工序查找还有没有后续工序没完成的前置工序
                        var prepIDs = curNode.PrepNodeIDs.Where(id =>
                                                             Edges.Any(e => e.SRC_NODE == id && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.NODE_ID == e.TGT_NODE && h.IsFinished))
                                                             Edges.Any(e => e.SRC_NODE == id && !wipSN.History.Any(h => h.WORK_ORDER == WO.ORDER_NO && h.UNBIND_FLAG != "Y" && h.NODE_ID == e.TGT_NODE && h.IsFinished))
                                                             ).ToList();
                        foreach (var prepID in prepIDs)
                        {
@@ -499,6 +586,131 @@
            return result;
        }
        /// <summary>
        /// 从工单中移除正在生产的条码
        /// </summary>
        /// <param name="wipList"></param>
        /// <returns></returns>
        public ApiAction RemoveWipSn(List<MES_WIP_DATA> wipList, string user)
        {
            var action = UnbindWipSnFromWO(WO, wipList, user);
            //移除成功则处理工单缓存中的数据
            if (action.IsSuccessed)
            {
                Update(true);
            }
            return action;
        }
        /// <summary>
        /// 从工单中解绑正在生产的条码
        /// </summary>
        /// <param name="wo"></param>
        /// <param name="wipList"></param>
        /// <returns></returns>
        public static ApiAction UnbindWipSnFromWO(BIZ_MES_WO wo, List<MES_WIP_DATA> wipList, string user)
        {
            var action = new ApiAction();
            var wipIDs = wipList.Select(q => q.ID);
            var batchs = wipList.Where(q => !q.BATCH_NO.IsNullOrEmpty()).GroupBy(q => new { q.BATCH_NO }).Select(g => new {
                g.Key.BATCH_NO,
                Qty = g.Count(),
                OutQty = g.Count(q => MES_WIP_DATA.STATUSs.Output.GetValue() <= q.STATUS && q.STATUS < MES_WIP_DATA.STATUSs.InStorage.GetValue()),
            });
            var db = Biz.Db;
            var dbTran = db.UseTran(() =>
            {
                //BIZ_MES_WO
                wo.INPUT_QTY -= wipList.Count;
                wo.OUTPUT_QTY -= wipList.Count(q => MES_WIP_DATA.STATUSs.Output.GetValue() <= q.STATUS && q.STATUS < MES_WIP_DATA.STATUSs.InStorage.GetValue());
                wo.SCRAP_QTY += wipList.Count;
                wo.STATUS = wo.STATUS.GetEnum<BIZ_MES_WO.STATUSs>() == BIZ_MES_WO.STATUSs.Closed ? BIZ_MES_WO.STATUSs.Working.GetValue() : wo.STATUS;
                db.Updateable(wo, user).UpdateColumns(q => new { q.INPUT_QTY, q.OUTPUT_QTY, q.SCRAP_QTY, q.UPDATE_USER, q.UPDATE_TIME, q.STATUS }).ExecuteCommand();
                //BIZ_MES_WO_BATCH
                var wobatchs = Biz.Db.Queryable<BIZ_MES_WO_BATCH>().Where(q => batchs.Select(x => x.BATCH_NO).Contains(q.BATCH_NO)).ToList();
                foreach (var batch in wobatchs)
                {
                    batch.INPUT_QTY -= batchs.Where(q=>q.BATCH_NO == batch.BATCH_NO).First().Qty;
                    batch.OUTPUT_QTY -= batchs.Where(q => q.BATCH_NO == batch.BATCH_NO).First().OutQty;
                    batch.SCRAP_QTY += batchs.Where(q => q.BATCH_NO == batch.BATCH_NO).First().Qty;
                    batch.STATUS = batch.STATUS.GetEnum<BIZ_MES_WO_BATCH.STATUSs>() == BIZ_MES_WO_BATCH.STATUSs.Closed ? BIZ_MES_WO_BATCH.STATUSs.Working.GetValue() : batch.STATUS;
                }
                db.Updateable(wobatchs, user).UpdateColumns(q => new { q.INPUT_QTY, q.OUTPUT_QTY, q.SCRAP_QTY, q.UPDATE_USER, q.UPDATE_TIME, q.STATUS }).ExecuteCommand();
                //BIZ_MES_WO_SN
                db.Updateable<BIZ_MES_WO_SN>(user)
                            .SetColumns(q => q.STATUS == BIZ_MES_WO_SN.STATUSs.Offline.GetValue())
                            .SetColumns(q => q.TRAY_SN == null).SetColumns(q => q.OUTER_SN == null)
                            .Where(q => q.WORK_ORDER == wo.ORDER_NO && wipIDs.Contains(q.WIP_ID))
                            .ExecuteCommand();
                //MES_WIP_DATA & MES_WIP_HIS
                var wipHiss = new List<MES_WIP_HIS>();
                foreach (var wipSN in wipList) //.Where(q => q.STATUS != MES_WIP_DATA.STATUSs.Offline.GetValue())
                {
                    wipSN.STATUS = MES_WIP_DATA.STATUSs.Offline.GetValue();
                    wipSN.TRAY_SN = wipSN.INNER_SN = wipSN.CARTON_SN = wipSN.PALLET_SN = wipSN.SHIPPING_ORDER = null;
                    wipSN.FINISHED_FLAG = wipSN.HOLD_FLAG = "N";
                    wipSN.UNBIND_FLAG = "Y";
                    wipSN.NODE_ID = "";
                    wipSN.NODE_NAME = "下线退库";
                    wipSN.OPERATION_TIME = DateTime.Now;
                    var his = new MES_WIP_HIS(wipSN, $"工单[{wipSN.WORK_ORDER}]条码[{wipSN.SN}]下线");
                    wipHiss.Add(his);
                }
                db.Storageable(wipList, user).ExecuteCommand();
                db.Storageable(wipHiss, user).ExecuteCommand();
                db.Updateable<MES_WIP_HIS>(user)
                           .SetColumns(q => q.UNBIND_FLAG == "Y")
                           .Where(q => q.WORK_ORDER == wo.ORDER_NO && wipIDs.Contains(q.WIP_ID))
                           .ExecuteCommand();
                //MES_WIP_PKG
                var curpkg = db.Queryable<MES_WIP_PKG>().Where(q => wipIDs.Contains(q.WIP_ID)).ToList();
                db.Deleteable(curpkg).ExecuteCommand();
                var pkgs = new List<MES_WIP_PKG>();
                do
                {
                    var parentSns = curpkg.Where(q => !q.PARENT_SN.IsNullOrEmpty()).Select(q => q.PARENT_SN).Distinct();
                    curpkg = db.Queryable<MES_WIP_PKG>().Where(q => parentSns.Contains(q.SN)).ToList();
                    foreach (var pkg in curpkg)
                    {
                        pkg.QTY = db.Queryable<MES_WIP_PKG>().Where(q => q.PARENT_SN == pkg.SN).Sum(q => q.QTY);
                        pkgs.Add(pkg);
                    }
                }
                while (curpkg.Any());
                db.Updateable(pkgs, user).ExecuteCommand();
                //MES_WIP_DFT,在上仓库装配和维修之前,先把不良记录标记为已处理
                db.Updateable<MES_WIP_DATA>(user)
                           .SetColumns(q => q.DFT_FLAG == "N")
                           .SetColumns(q => q.DFT_COUNT == 0)
                           .SetColumns(q => q.DFT_CODE == null)
                           .Where(q => q.WORK_ORDER == wo.ORDER_NO && wipIDs.Contains(q.ID))
                           .ExecuteCommand();
                db.Updateable<MES_WIP_DFT>(user)
                          .SetColumns(q => q.STATUS == MES_WIP_DFT.STATUSs.Resolved.GetValue())
                          .Where(q => q.WORK_ORDER == wo.ORDER_NO && wipIDs.Contains(q.WIP_ID))
                          .ExecuteCommand();
            });
            if (!dbTran.IsSuccess)
            {
                //抛出异常
                throw dbTran.ErrorException;
            }
            //更新工单池中的工单条码表数据的状态
            foreach (var item in WoContext.WoBatchDic)
            {
                if (item.Value.WO.ORDER_NO == wo.ORDER_NO)
                {
                    foreach (var woSn in item.Value.WoSNs.Where(q => wipIDs.Contains(q.WIP_ID)))
                    {
                        woSn.STATUS = BIZ_MES_WO_SN.STATUSs.Offline.GetValue();
                    }
                    item.Value.Update();
                }
            }
            return action;
        }
        #endregion
        /// <summary>