using Yitter.IdGenerator; namespace Admin.NET.Plugin.AiDOP.Order; /// /// 产品设计服务(常规/非标;合同维度跟踪设计负责人、图号、BOM、工艺、图纸计划/实际时间) /// 路由前缀:/api/Order/productdesign/... /// [ApiDescriptionSettings(Order = 255, Description = "产品设计")] [Route("api/Order")] [AllowAnonymous] [NonUnify] public class ProductDesignService : IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly SqlSugarRepository _designRep; private readonly SqlSugarRepository _bomRep; private readonly SqlSugarRepository _routingRep; private readonly UserManager _userManager; public ProductDesignService( ISqlSugarClient db, SqlSugarRepository designRep, SqlSugarRepository bomRep, SqlSugarRepository routingRep, UserManager userManager) { _db = db; _designRep = designRep; _bomRep = bomRep; _routingRep = routingRep; _userManager = userManager; } [DisplayName("获取产品设计列表")] [HttpGet("productdesign/list")] public async Task GetProductDesignList([FromQuery] ProductDesignListInput input) { var q = _db.Queryable() .WhereIF(!string.IsNullOrWhiteSpace(input.BillNo), u => u.BillNo.Contains(input.BillNo!.Trim())) .WhereIF(!string.IsNullOrWhiteSpace(input.ContractNo), u => u.ContractNo != null && u.ContractNo.Contains(input.ContractNo!.Trim())) .WhereIF(input.ProductKind is 1 or 2, u => u.ProductKind == input.ProductKind!.Value) .WhereIF(!string.IsNullOrWhiteSpace(input.DesignLeadName), u => u.DesignLeadName != null && u.DesignLeadName.Contains(input.DesignLeadName!.Trim())); var paged = await q.OrderByDescending(u => u.Id).ToPagedListAsync(input.Page, input.PageSize); var list = paged.Items.Select(u => new { id = u.Id, billNo = u.BillNo, contractNo = u.ContractNo, productKind = u.ProductKind, designLeadAccount = u.DesignLeadAccount, designLeadName = u.DesignLeadName, drawingNo = u.DrawingNo, drawingPlanStart = u.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm"), drawingPlanEnd = u.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm"), drawingActualStart = u.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm"), drawingActualEnd = u.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm"), applicant = u.Applicant, applyDate = u.ApplyDate?.ToString("yyyy-MM-dd"), productModel = u.ProductModel, itemNum = u.ItemNum, createTime = u.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"), updateTime = u.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"), }).ToList(); return new { total = paged.Total, page = input.Page, pageSize = input.PageSize, list }; } [DisplayName("获取产品设计详情")] [HttpGet("productdesign/{id:long}")] public async Task GetProductDesignDetail(long id) { var m = await _designRep.GetFirstAsync(u => u.Id == id) ?? throw Oops.Oh("产品设计记录不存在"); var boms = await _bomRep.GetListAsync(u => u.ProductDesignId == id); var routings = await _routingRep.GetListAsync(u => u.ProductDesignId == id); return new { id = m.Id, billNo = m.BillNo, contractNo = m.ContractNo, productKind = m.ProductKind, designLeadAccount = m.DesignLeadAccount, designLeadName = m.DesignLeadName, drawingNo = m.DrawingNo, drawingPlanStart = m.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm:ss"), drawingPlanEnd = m.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm:ss"), drawingActualStart = m.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm:ss"), drawingActualEnd = m.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm:ss"), applicant = m.Applicant, applyDate = m.ApplyDate?.ToString("yyyy-MM-dd"), productModel = m.ProductModel, itemNum = m.ItemNum, language = m.Language, lineRemark = m.LineRemark, createUser = m.CreateUser, createTime = m.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"), updateUser = m.UpdateUser, updateTime = m.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"), boms = boms.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new { id = x.Id.ToString(), parentBomId = x.ParentBomId.HasValue ? x.ParentBomId.Value.ToString() : null, seq = x.Seq, itemNum = x.ItemNum, itemName = x.ItemName, processCode = x.ProcessCode, fixedLossQty = x.FixedLossQty, batchNo = x.BatchNo, }), routings = routings.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new { id = x.Id, seq = x.Seq, opName = x.OpName, opCode = x.OpCode, isKeyProcess = x.IsKeyProcess, productionLine = x.ProductionLine, routeCode = x.RouteCode, }), }; } [DisplayName("保存产品设计")] [ApiDescriptionSettings(Name = "SaveProductDesign"), HttpPost("productdesign/save")] public async Task SaveProductDesign([FromBody] ProductDesignSaveInput input) { if (input.ProductKind is not (1 or 2)) throw Oops.Oh("产品类型无效,请选择常规产品或非标产品"); var now = DateTime.Now; var user = _userManager.Account ?? "system"; await _db.Ado.BeginTranAsync(); try { long designId; if (input.Id is null or 0) { var month = now.ToString("yyyyMM"); var maxSeq = await _db.Ado.GetIntAsync( $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_product_design WHERE BillNo LIKE 'PD{month}%'"); var billNo = $"PD{month}{(maxSeq + 1):D4}"; designId = YitIdHelper.NextId(); var entity = MapMaster(new ProductDesign { Id = designId, BillNo = billNo }, input, user, now, isNew: true); await _designRep.InsertAsync(entity); await SaveBomsAsync(designId, input.Boms ?? new List(), isNew: true); await SaveRoutingsAsync(designId, input.Routings ?? new List(), isNew: true); await _db.Ado.CommitTranAsync(); return new { id = designId, billNo, message = "新增成功" }; } var existing = await _designRep.GetFirstAsync(u => u.Id == input.Id!.Value) ?? throw Oops.Oh("产品设计记录不存在"); designId = existing.Id; MapMaster(existing, input, user, now, isNew: false); await _designRep.UpdateAsync(existing); await SaveBomsAsync(designId, input.Boms ?? new List(), isNew: false); await SaveRoutingsAsync(designId, input.Routings ?? new List(), isNew: false); await _db.Ado.CommitTranAsync(); return new { id = designId, billNo = existing.BillNo, message = "编辑成功" }; } catch { await _db.Ado.RollbackTranAsync(); throw; } } [DisplayName("删除产品设计")] [ApiDescriptionSettings(Name = "DeleteProductDesign"), HttpPost("productdesign/delete")] public async Task DeleteProductDesign([FromBody] ProductDesignDeleteInput input) { var entity = await _designRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh("产品设计记录不存在"); await _db.Ado.BeginTranAsync(); try { await _bomRep.DeleteAsync(u => u.ProductDesignId == entity.Id); await _routingRep.DeleteAsync(u => u.ProductDesignId == entity.Id); await _designRep.DeleteAsync(u => u.Id == entity.Id); await _db.Ado.CommitTranAsync(); return new { message = "删除成功" }; } catch { await _db.Ado.RollbackTranAsync(); throw; } } private static ProductDesign MapMaster(ProductDesign entity, ProductDesignSaveInput input, string user, DateTime now, bool isNew) { entity.ContractNo = string.IsNullOrWhiteSpace(input.ContractNo) ? null : input.ContractNo.Trim(); entity.ProductKind = input.ProductKind; entity.DesignLeadAccount = string.IsNullOrWhiteSpace(input.DesignLeadAccount) ? null : input.DesignLeadAccount.Trim(); entity.DesignLeadName = string.IsNullOrWhiteSpace(input.DesignLeadName) ? null : input.DesignLeadName.Trim(); entity.DrawingNo = string.IsNullOrWhiteSpace(input.DrawingNo) ? null : input.DrawingNo.Trim(); entity.DrawingPlanStart = ParseOptionalDate(input.DrawingPlanStart); entity.DrawingPlanEnd = ParseOptionalDate(input.DrawingPlanEnd); entity.DrawingActualStart = ParseOptionalDate(input.DrawingActualStart); entity.DrawingActualEnd = ParseOptionalDate(input.DrawingActualEnd); entity.Applicant = string.IsNullOrWhiteSpace(input.Applicant) ? null : input.Applicant.Trim(); entity.ApplyDate = ParseOptionalDate(input.ApplyDate); entity.ProductModel = string.IsNullOrWhiteSpace(input.ProductModel) ? null : input.ProductModel.Trim(); entity.ItemNum = string.IsNullOrWhiteSpace(input.ItemNum) ? null : input.ItemNum.Trim(); entity.Language = string.IsNullOrWhiteSpace(input.Language) ? null : input.Language.Trim(); entity.Qty = null; entity.LineRemark = string.IsNullOrWhiteSpace(input.LineRemark) ? null : input.LineRemark.Trim(); if (isNew) { entity.CreateUser = user; entity.CreateTime = now; entity.IsActive = 1; } else { entity.UpdateUser = user; entity.UpdateTime = now; } return entity; } private static DateTime? ParseOptionalDate(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; return DateTime.TryParse(s.Trim(), out var dt) ? dt : null; } private async Task SaveBomsAsync(long designId, List items, bool isNew) { var dbRows = isNew ? new List() : await _bomRep.GetListAsync(u => u.ProductDesignId == designId); var dbById = dbRows.ToDictionary(u => u.Id); if (items.Any(x => !x.Id.HasValue)) throw Oops.Oh("BOM 每行必须包含 Id(已保存行为正数,新行请使用负数临时 Id)"); var ordered = OrderBomsForSave(items); var tempToReal = new Dictionary(); var keptIds = new HashSet(); var idx = 0; foreach (var d in ordered) { idx++; var seq = d.Seq ?? idx; var parentResolved = ResolveBomParentId(d.ParentBomId, tempToReal); if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing)) { if (existing.ProductDesignId != designId) continue; existing.ParentBomId = parentResolved; existing.Seq = seq; existing.ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim(); existing.ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim(); existing.ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim(); existing.Qty = null; existing.FixedLossQty = d.FixedLossQty; existing.BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim(); await _bomRep.UpdateAsync(existing); keptIds.Add(existing.Id); } else if (d.Id is < 0) { var newId = YitIdHelper.NextId(); tempToReal[d.Id.Value] = newId; var row = new ProductDesignBom { Id = newId, ProductDesignId = designId, ParentBomId = parentResolved, Seq = seq, ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim(), ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim(), ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim(), Qty = null, FixedLossQty = d.FixedLossQty, BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim(), }; await _bomRep.InsertAsync(row); keptIds.Add(newId); } } foreach (var row in dbRows.Where(u => !keptIds.Contains(u.Id))) await _bomRep.DeleteAsync(u => u.Id == row.Id); } /// 按父子顺序排列:父(含库中已有 Id)先于子;负数临时 Id 先于引用它的子行。 private static List OrderBomsForSave(List items) { var withId = items.Where(x => x.Id.HasValue).ToList(); var inBatch = withId.Select(x => x.Id!.Value).ToHashSet(); var done = new HashSet(); var result = new List(); var remaining = withId.ToList(); bool CanTake(ProductDesignBomInput r) { var p = r.ParentBomId; if (p is null or 0) return true; if (p < 0) return done.Contains(p.Value); if (inBatch.Contains(p.Value)) return done.Contains(p.Value); return true; } while (remaining.Count > 0) { var batch = remaining.Where(CanTake).ToList(); if (batch.Count == 0) throw Oops.Oh("BOM 父子关系无效(循环或缺失父节点)"); foreach (var b in batch) { result.Add(b); remaining.Remove(b); if (b.Id.HasValue) done.Add(b.Id.Value); } } return result; } private static long? ResolveBomParentId(long? parentBomId, Dictionary tempToReal) { if (parentBomId is null or 0) return null; if (parentBomId.Value < 0) { if (tempToReal.TryGetValue(parentBomId.Value, out var real)) return real; throw Oops.Oh("BOM 父节点未找到,请检查父子顺序"); } return parentBomId; } private async Task SaveRoutingsAsync(long designId, List items, bool isNew) { var dbRows = isNew ? new List() : await _routingRep.GetListAsync(u => u.ProductDesignId == designId); var dbById = dbRows.ToDictionary(u => u.Id); var inputIds = new HashSet(items.Where(d => d.Id is > 0).Select(d => d.Id!.Value)); var idx = 0; foreach (var d in items) { idx++; var seq = d.Seq ?? idx; if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing)) { if (existing.ProductDesignId != designId) continue; existing.Seq = seq; existing.OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim(); existing.OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim(); existing.ParentOpCode = null; existing.IsKeyProcess = d.IsKeyProcess; existing.ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim(); existing.RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim(); await _routingRep.UpdateAsync(existing); } else { var row = new ProductDesignRouting { Id = YitIdHelper.NextId(), ProductDesignId = designId, Seq = seq, OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim(), OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim(), ParentOpCode = null, IsKeyProcess = d.IsKeyProcess, ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim(), RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim(), }; await _routingRep.InsertAsync(row); } } foreach (var toDelete in dbRows.Where(u => !inputIds.Contains(u.Id))) await _routingRep.DeleteAsync(u => u.Id == toDelete.Id); } }