服务端的TigerApi 框架,基于.NET6 2024 版本
Rodney Chen
2024-12-25 6c1b1e0601a7db6b576aaa180d6777418a404f18
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
using Tiger.Model;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using Rhea.Common;
using System.Linq;
using Tiger.IBusiness;
using Tiger.Model.Entitys.MES.Position;
using Tiger.Business.MES;
using Org.BouncyCastle.Ocsp;
using System.IO;
 
namespace Tiger.Business
{
    /// <summary>
    /// 生产中的工单批次
    /// </summary>
    public partial class WorkBatch : IWorkBatch, IDisposable
    {
        public WorkBatch(string order)
        {
            OrderNo = order;
        }
        #region Propertys & Variables
        public string OrderNo { get; set; }
        public string LineCode { get; set; }
        public BIZ_MES_WO WO { get; set; }
        public BAS_ITEM Product { get; set; }
        public BIZ_MES_WO_BATCH Batch { get; set; }
        public BAS_CUSTOMER Customer { get; set; }
        public List<BIZ_MES_WO_SN> WoSNs { get; set; }
        public List<MES_WO_EDGE> Edges { get; set; }
        public List<MES_WO_NODE> Nodes { get; set; }
        public List<MES_WO_OPER> NodeSets { get; set; }
        public List<MES_WO_NODE_ACT> NodeActs { get; set; }
        public List<MES_WO_ACTION> ActionSets { get; set; }
        public List<MES_WO_NODE_POST> NodePosts { get; set; }
        public List<MES_WO_NODE_DFTG> NodeDftgs { get; set; }
        public List<BAS_DEFECT_GRP> DefectGroups { get; set; }
        public List<BAS_DEFECT> Defects => DefectGroups.SelectMany(q => q.Defects).ToList();
        /// <summary>
        /// 事务锁
        /// </summary>
        public object TransLock { get; }
        #endregion
 
        #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;
        }
 
        /// <summary>
        /// 更新工单批次资料
        /// </summary>
        /// <param name="updateAll"></param>
        public void Update(bool updateAll = false)
        {
            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);
            }
            WO = wo;
            Batch = batch;
            ConsoleExt.WriteLine($"{DateTime.Now:HH:mm:ss} ##### : {(DateTime.Now - strat).TotalSeconds}s", ConsoleColor.Yellow);
        }
 
        private void GetBatchInfo()
        {
            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();
            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();
            Nodes = Biz.Db.Queryable<MES_WO_NODE>().Where(q => q.WORK_ORDER == OrderNo).IncludesAllFirstLayer().ToList();
            NodeSets = Biz.Db.Queryable<MES_WO_OPER>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            NodeActs = Biz.Db.Queryable<MES_WO_NODE_ACT>().Where(q => q.WORK_ORDER == OrderNo).IncludesAllFirstLayer().ToList();
            ActionSets = Biz.Db.Queryable<MES_WO_ACTION>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            NodePosts = Biz.Db.Queryable<MES_WO_NODE_POST>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            NodeDftgs = Biz.Db.Queryable<MES_WO_NODE_DFTG>().Where(q => q.WORK_ORDER == OrderNo).ToList();
            DefectGroups = Biz.Db.Queryable<BAS_DEFECT_GRP>().IncludesAllFirstLayer().ToList();
 
            //工序节点排序
            var first = Nodes.First(q => q.IS_FIRST_NODE == "Y");
            first.Sequence = 1;
            NodeSorting(first);
        }
 
        /// <summary>
        /// 递归遍历工序节点,给下级工序节点排序和整理前置工序
        /// </summary>
        /// <param name="parent"></param>
        private void NodeSorting(MES_WO_NODE parent)
        {
            var edges = Edges.Where(q => q.SRC_NODE == parent.ID && Nodes.Any(a => a.ID == q.TGT_NODE)).ToList();
            foreach (var edge in edges)
            {
                var next = Nodes.First(q => q.ID == edge.TGT_NODE);
                //排除维修工序
                if (next.Operation.OPER_TYPE != MES_OPERATION.OPER_TYPEs.Repair.GetValue())
                {
                    next.Sequence = next.Sequence > parent.Sequence ? next.Sequence : (parent.Sequence + 1);
                    //如果父节点启用且不可跳站,则添加为下一个工序的前置工序
                    var setting = NodeSets.FirstOrDefault(q => q.NODE_ID == parent.ID);
                    if (!setting.IsNullOrEmpty() && setting.IS_ACTIVE == "Y" && setting.CAN_SKIP != "Y")
                    {
                        next.PrepNodeIDs.Add(parent.ID);
                    }
                    //继承父结点的前置工序
                    next.PrepNodeIDs.AddRange(parent.PrepNodeIDs);
                    next.PrepNodeIDs = next.PrepNodeIDs.Distinct().ToList();
                    NodeSorting(next);
                }
            }
        }
 
        /// <summary>
        /// 检查工单在当前工序节点是否可以生产
        /// </summary>
        /// <param name="curNode"></param>
        /// <returns></returns>
        public ApiAction<SubmitOutput> CheckCanProduce(MES_WO_NODE curNode, MES_WO_OPER curNodeSetting)
        {
            var action = new ApiAction<SubmitOutput>(new SubmitOutput(), true);
 
            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.GetEnumDesc<BIZ_MES_WO_BATCH.STATUSs>());
            }
            //工单状态不是已下发或者生产中,则不允许生产
            if (WO.STATUS != BIZ_MES_WO.STATUSs.Release.GetValue() && WO.STATUS != BIZ_MES_WO.STATUSs.Working.GetValue())
            {
                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.GetEnumDesc<BIZ_MES_WO.STATUSs>());
            }
            //工单批次投入数量减去报废数量如果大于等于计划数量,则不允许生产
            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, 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);
            }
            //工单投入数量减去报废数量如果大于等于计划数量,则不允许生产
            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, 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;
        }
 
        /// <summary>
        /// 工单开工
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public bool StartWorking(string user)
        {
            if (Batch.STATUS == BIZ_MES_WO_BATCH.STATUSs.Release.GetValue() || Batch.STATUS == BIZ_MES_WO_BATCH.STATUSs.Paused.GetValue())
            {
                WO.STATUS = BIZ_MES_WO.STATUSs.Working.GetValue();
                WO.ACT_START_TIME = WO.ACT_START_TIME < new DateTime(2000, 1, 1) ? DateTime.Now : WO.ACT_START_TIME;
                Batch.STATUS = BIZ_MES_WO_BATCH.STATUSs.Working.GetValue();
                Batch.ACT_START_TIME = Batch.ACT_START_TIME < new DateTime(2000, 1, 1) ? DateTime.Now : Batch.ACT_START_TIME;
                //保存数据库
                var db = Biz.Db;
                var dbTran = db.UseTran(() =>
                {
                    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)
                {
                    //throw dbTran.ErrorException;
                    return false;
                }
            }
            return true;
        }
 
        /// <summary>
        /// 工单暂停
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public bool PausedWorking(string user)
        {
            if (Batch.STATUS == BIZ_MES_WO_BATCH.STATUSs.Working.GetValue())
            {
                WO.STATUS = BIZ_MES_WO.STATUSs.Paused.GetValue();
                Batch.STATUS = BIZ_MES_WO_BATCH.STATUSs.Paused.GetValue();
                //保存数据库
                var db = Biz.Db;
                var dbTran = db.UseTran(() =>
                {
                    db.Updateable(WO, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS }).ExecuteCommand();
                    db.Updateable(Batch, user).UpdateColumns(q => new { q.UPDATE_TIME, q.UPDATE_USER, q.STATUS }).ExecuteCommand();
                });
                if (!dbTran.IsSuccess)
                {
                    //throw dbTran.ErrorException;
                    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;
        }
 
        /// <summary>
        /// 根据岗位编码判断是不是首站
        /// </summary>
        /// <param name="postCode"></param>
        /// <returns></returns>
        public bool IsFirstNode(string postCode)
        {
            return Nodes.Any(q => q.IS_FIRST_NODE == "Y" && NodePosts.Any(p => p.NODE_ID == q.ID && p.POST_CODE == postCode));
        }
 
        /// <summary>
        /// 根据岗位编码返回工序节点
        /// </summary>
        /// <param name="postCode"></param>
        /// <returns></returns>
        public MES_WO_NODE GetNode(string postCode)
        {
            return Nodes.FirstOrDefault(q => NodePosts.Any(p => p.NODE_ID == q.ID && p.POST_CODE == postCode));
        }
 
        /// <summary>
        /// 根据工序节点返回工序节点设置
        /// </summary>
        /// <param name="postCode"></param>
        /// <returns></returns>
        public MES_WO_OPER GetNodeSetting(MES_WO_NODE node)
        {
            return NodeSets.FirstOrDefault(q => q.NODE_ID == node?.ID);
        }
 
        /// <summary>
        /// 根据传入的条码过站信息和下一站的目标工序,判断条码是否能进入下一站
        /// </summary>
        /// <param name="input">本次提交的数据</param>
        /// <param name="wipSN">当前的条码过站记录,需要导航查询生产过程记录信息</param>
        /// <param name="nextNode">要进入的目标工序</param>
        /// <returns></returns>
        public ApiAction<SubmitOutput> CanGotoNext(SubmitInput input, MES_WIP_DATA wipSN, MES_WO_NODE nextNode)
        {
            var action = new ApiAction<SubmitOutput>(new SubmitOutput());
 
            //条码在本工单第一次过站
            if (wipSN.NODE_ID.IsNullOrEmpty())
            {
                if (nextNode.IS_FIRST_NODE == "Y")
                {
                    action.IsSuccessed = true;
                    return action;
                }
                else
                {
                    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;
                }
            }
            //条码已有过站记录
            else
            {
                var curNode = Nodes.First(q => q.ID == wipSN.NODE_ID);
                var curSetting = NodeSets.FirstOrDefault(q => q.NODE_ID == curNode.ID);
                var nextSetting = NodeSets.FirstOrDefault(q => q.NODE_ID == nextNode.ID);
                //进入维修:如果目标工序是维修工序则判断当前条码在本工单有未维修的不良记录且当前工序节点有连线到维修工序,则允许进入维修工序
                if (nextNode.Operation.OPER_TYPE == MES_OPERATION.OPER_TYPEs.Repair.GetValue()
                    && wipSN.Defects.Any(q => q.WORK_ORDER == WO.ORDER_NO && q.STATUS < MES_WIP_DFT.STATUSs.Resolved.GetValue())
                    && Edges.Any(q => q.SRC_NODE == curNode.ID && q.TGT_NODE == nextNode.ID))
                {
                    action.IsSuccessed = true;
                    return action;
                }
                //维修回流:如果条码的当前工序是维修工序,则认为是维修回流
                else if (curNode.Operation.OPER_TYPE == MES_OPERATION.OPER_TYPEs.Repair.GetValue())
                {
                    //查找所有可以回流的工序
                    var reflowNodes = Nodes.Where(q => Edges.Any(e => e.SRC_NODE == curNode.ID && e.TGT_NODE == q.ID)
                                                                && NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y")).ToList();
 
                    //可回流的工序包含目标工序且条码在维修站选择的回流工序为空或者等于目标工序,则允许进站
                    if (reflowNodes.Any(q => q.ID == nextNode.ID) && (wipSN.REFLOW_NODE.IsNullOrEmpty() || wipSN.REFLOW_NODE == nextNode.NODE_NAME))
                    {
                        action.IsSuccessed = true;
                        return action;
                    }
                    else
                    {
                        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.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);
                    //如果下一个可执行工序包含目标工序则允许进站
                    if (nextNodes.Any(q => q.ID == nextNode.ID))
                    {
                        action.IsSuccessed = true;
                        return action;
                    }
 
                    //如果当前工序有必须执行的后续工序,则报错
                    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"));
                        //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;
                    }
                    //如果当前工序没有必须执行的后续工序,则在前置工序查找还有没有后续工序没完成的工序,有则尝试执行
                    else
                    {
                        //在前置工序查找还有没有后续工序没完成的前置工序
                        var prepIDs = curNode.PrepNodeIDs.Where(id =>
                                                             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)
                        {
                            //如果连线的目标工序的前置工序都已完成,则把连线的目标工序添加到可执行工序列表
                            var prep = Nodes.First(q => q.ID == prepID);
                            var next = GetNextNodes(prep, wipSN);
                            nextNodes.AddRange(next);
                        }
 
                        //如果下一个可执行工序包含目标工序则允许进站
                        if (nextNodes.Any(q => q.ID == nextNode.ID))
                        {
                            action.IsSuccessed = true;
                            return action;
                        }
                        else
                        {
                            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;
                        }
                    }
                }
            }
 
            return action;
        }
 
        /// <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()
                                                                && Edges.Any(e => e.SRC_NODE == parent.ID && e.TGT_NODE == q.ID)
                                                                && !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.UNBIND_FLAG != "Y" && h.NODE_ID == id && h.IsFinished)) || parent.IS_FIRST_NODE == "Y")
                {
                    var setting = NodeSets.FirstOrDefault(q => q.NODE_ID == next.ID);
                    //后续工序是启用的,则添加
                    if (!result.Any(q => q.ID == next.ID) && setting.IS_ACTIVE == "Y")
                    {
                        result.Add(next);
                    }
                    //后续工序是不启用或者可跳站,则继续添加其后续工序
                    if (!setting.IsNullOrEmpty() && (setting.IS_ACTIVE != "Y" || setting.CAN_SKIP == "Y"))
                    {
                        result.AddRange(GetNextNodes(next, wipSN));
                    }
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// 根据传入的条码返回下一站可进入的目标工序
        /// </summary>
        /// <param name="wipSN">当前的条码过站记录,需要导航查询生产过程记录和生产不良记录信息</param>
        /// <returns></returns>
        public List<MES_WO_NODE> GetNextNodes(MES_WIP_DATA wipSN)
        {
            var result = new List<MES_WO_NODE>();
 
            //条码在本工单第一次过站
            if (wipSN.NODE_ID.IsNullOrEmpty())
            {
                result.AddRange(Nodes.Where(q => q.IS_FIRST_NODE == "Y"));
            }
            //条码已有过站记录
            else
            {
                var curNode = Nodes.First(q => q.ID == wipSN.NODE_ID);
                //条码在维修工序,返回可回流工序
                if (curNode.Operation.OPER_TYPE == MES_OPERATION.OPER_TYPEs.Repair.GetValue())
                {
                    //查找所有可以回流的工序
                    var reflowNodes = Nodes.Where(q => Edges.Any(e => e.SRC_NODE == curNode.ID && e.TGT_NODE == q.ID)
                                                                && NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y")).ToList();
                    result.AddRange(reflowNodes.Where(q => wipSN.REFLOW_NODE.IsNullOrEmpty() || wipSN.REFLOW_NODE == q.NODE_NAME));
                }
                else
                {
                    //往下查找条码当前工序的下一个可执行工序
                    var nextNodes = GetNextNodes(curNode, wipSN);
                    //如果当前工序没有必须执行的后续工序,则尝试在前置工序查找还有没有后续工序没完成的工序,有则加入
                    if (!nextNodes.Any(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.IS_ACTIVE == "Y" && s.CAN_SKIP != "Y")))
                    {
                        //在前置工序查找还有没有后续工序没完成的前置工序
                        var prepIDs = curNode.PrepNodeIDs.Where(id =>
                                                             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)
                        {
                            //如果连线的目标工序的前置工序都已完成,则把连线的目标工序添加到可执行工序列表
                            var prep = Nodes.First(q => q.ID == prepID);
                            var next = GetNextNodes(prep, wipSN);
                            nextNodes.AddRange(next);
                        }
                    }
                    result.AddRange(nextNodes);
                    //如果当前条码是不良则只返回允许不良进站的工序和工序连接的维修工序
                    if (wipSN.Defects.Any(q => q.WORK_ORDER == WO.ORDER_NO && q.STATUS < MES_WIP_DFT.STATUSs.Resolved.GetValue()))
                    {
                        result.RemoveAll(q => NodeSets.Any(s => s.NODE_ID == q.ID && s.ALLOW_DFT_IN != "Y"));
                        //加入工序连接的维修工序
                        var repairNodes = Nodes.Where(q => Edges.Any(e => e.SRC_NODE == curNode.ID && e.TGT_NODE == q.ID) && q.Operation.OPER_TYPE == MES_OPERATION.OPER_TYPEs.Repair.GetValue());
                        result.AddRange(repairNodes);
                    }
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// 根据岗位编码返回工序不良代码
        /// </summary>
        /// <param name="postCode"></param>
        /// <returns></returns>
        public List<DefectOutput> GetNodeDefects(string postCode)
        {
            var result = new List<DefectOutput>();
            var groups = DefectGroups.Where((b, s) => NodeDftgs.Any(d => d.NODE_ID == GetNode(postCode).ID && d.DFTG_CODE == b.DFTG_CODE)).ToList();
            foreach (var group in groups)
            {
                result.AddRange(group.Defects.Select(q => new DefectOutput
                {
                    DFTG_CODE = q.DFTG_CODE,
                    DFTG_NAME = group.DFTG_NAME,
                    DFT_CODE = q.DFT_CODE,
                    DFT_NAME = q.DFT_NAME,
                    DFT_LEVEL = q.DFT_LEVEL,
                }));
            }
            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>
        /// 关闭工单
        /// </summary>
        /// <returns></returns>
        public bool Close()
        {
            Dispose();
            return true;
        }
        public virtual void Dispose()
        {
            WoContext.WoBatchDic.Remove(OrderNo);
        }
    }
}