ProductDesignService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. using Yitter.IdGenerator;
  2. namespace Admin.NET.Plugin.AiDOP.Order;
  3. /// <summary>
  4. /// 产品设计服务(常规/非标;合同维度跟踪设计负责人、图号、BOM、工艺、图纸计划/实际时间)
  5. /// 路由前缀:/api/Order/productdesign/...
  6. /// </summary>
  7. [ApiDescriptionSettings(Order = 255, Description = "产品设计")]
  8. [Route("api/Order")]
  9. [AllowAnonymous]
  10. [NonUnify]
  11. public class ProductDesignService : IDynamicApiController, ITransient
  12. {
  13. private readonly ISqlSugarClient _db;
  14. private readonly SqlSugarRepository<ProductDesign> _designRep;
  15. private readonly SqlSugarRepository<ProductDesignBom> _bomRep;
  16. private readonly SqlSugarRepository<ProductDesignRouting> _routingRep;
  17. private readonly UserManager _userManager;
  18. public ProductDesignService(
  19. ISqlSugarClient db,
  20. SqlSugarRepository<ProductDesign> designRep,
  21. SqlSugarRepository<ProductDesignBom> bomRep,
  22. SqlSugarRepository<ProductDesignRouting> routingRep,
  23. UserManager userManager)
  24. {
  25. _db = db;
  26. _designRep = designRep;
  27. _bomRep = bomRep;
  28. _routingRep = routingRep;
  29. _userManager = userManager;
  30. }
  31. [DisplayName("获取产品设计列表")]
  32. [HttpGet("productdesign/list")]
  33. public async Task<object> GetProductDesignList([FromQuery] ProductDesignListInput input)
  34. {
  35. var q = _db.Queryable<ProductDesign>()
  36. .WhereIF(!string.IsNullOrWhiteSpace(input.BillNo), u => u.BillNo.Contains(input.BillNo!.Trim()))
  37. .WhereIF(!string.IsNullOrWhiteSpace(input.ContractNo), u => u.ContractNo != null && u.ContractNo.Contains(input.ContractNo!.Trim()))
  38. .WhereIF(input.ProductKind is 1 or 2, u => u.ProductKind == input.ProductKind!.Value)
  39. .WhereIF(!string.IsNullOrWhiteSpace(input.DesignLeadName), u => u.DesignLeadName != null && u.DesignLeadName.Contains(input.DesignLeadName!.Trim()));
  40. var paged = await q.OrderByDescending(u => u.Id).ToPagedListAsync(input.Page, input.PageSize);
  41. var list = paged.Items.Select(u => new
  42. {
  43. id = u.Id,
  44. billNo = u.BillNo,
  45. contractNo = u.ContractNo,
  46. productKind = u.ProductKind,
  47. designLeadAccount = u.DesignLeadAccount,
  48. designLeadName = u.DesignLeadName,
  49. drawingNo = u.DrawingNo,
  50. drawingPlanStart = u.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm"),
  51. drawingPlanEnd = u.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm"),
  52. drawingActualStart = u.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm"),
  53. drawingActualEnd = u.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm"),
  54. applicant = u.Applicant,
  55. applyDate = u.ApplyDate?.ToString("yyyy-MM-dd"),
  56. productModel = u.ProductModel,
  57. itemNum = u.ItemNum,
  58. createTime = u.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  59. updateTime = u.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  60. }).ToList();
  61. return new { total = paged.Total, page = input.Page, pageSize = input.PageSize, list };
  62. }
  63. [DisplayName("获取产品设计详情")]
  64. [HttpGet("productdesign/{id:long}")]
  65. public async Task<object> GetProductDesignDetail(long id)
  66. {
  67. var m = await _designRep.GetFirstAsync(u => u.Id == id)
  68. ?? throw Oops.Oh("产品设计记录不存在");
  69. var boms = await _bomRep.GetListAsync(u => u.ProductDesignId == id);
  70. var routings = await _routingRep.GetListAsync(u => u.ProductDesignId == id);
  71. return new
  72. {
  73. id = m.Id,
  74. billNo = m.BillNo,
  75. contractNo = m.ContractNo,
  76. productKind = m.ProductKind,
  77. designLeadAccount = m.DesignLeadAccount,
  78. designLeadName = m.DesignLeadName,
  79. drawingNo = m.DrawingNo,
  80. drawingPlanStart = m.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm:ss"),
  81. drawingPlanEnd = m.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
  82. drawingActualStart = m.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm:ss"),
  83. drawingActualEnd = m.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
  84. applicant = m.Applicant,
  85. applyDate = m.ApplyDate?.ToString("yyyy-MM-dd"),
  86. productModel = m.ProductModel,
  87. itemNum = m.ItemNum,
  88. language = m.Language,
  89. lineRemark = m.LineRemark,
  90. createUser = m.CreateUser,
  91. createTime = m.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  92. updateUser = m.UpdateUser,
  93. updateTime = m.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  94. boms = boms.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new
  95. {
  96. id = x.Id.ToString(),
  97. parentBomId = x.ParentBomId.HasValue ? x.ParentBomId.Value.ToString() : null,
  98. seq = x.Seq,
  99. itemNum = x.ItemNum,
  100. itemName = x.ItemName,
  101. processCode = x.ProcessCode,
  102. fixedLossQty = x.FixedLossQty,
  103. batchNo = x.BatchNo,
  104. }),
  105. routings = routings.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new
  106. {
  107. id = x.Id,
  108. seq = x.Seq,
  109. opName = x.OpName,
  110. opCode = x.OpCode,
  111. isKeyProcess = x.IsKeyProcess,
  112. productionLine = x.ProductionLine,
  113. routeCode = x.RouteCode,
  114. }),
  115. };
  116. }
  117. [DisplayName("保存产品设计")]
  118. [ApiDescriptionSettings(Name = "SaveProductDesign"), HttpPost("productdesign/save")]
  119. public async Task<object> SaveProductDesign([FromBody] ProductDesignSaveInput input)
  120. {
  121. if (input.ProductKind is not (1 or 2))
  122. throw Oops.Oh("产品类型无效,请选择常规产品或非标产品");
  123. var now = DateTime.Now;
  124. var user = _userManager.Account ?? "system";
  125. await _db.Ado.BeginTranAsync();
  126. try
  127. {
  128. long designId;
  129. if (input.Id is null or 0)
  130. {
  131. var month = now.ToString("yyyyMM");
  132. var maxSeq = await _db.Ado.GetIntAsync(
  133. $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_product_design WHERE BillNo LIKE 'PD{month}%'");
  134. var billNo = $"PD{month}{(maxSeq + 1):D4}";
  135. designId = YitIdHelper.NextId();
  136. var entity = MapMaster(new ProductDesign { Id = designId, BillNo = billNo }, input, user, now, isNew: true);
  137. await _designRep.InsertAsync(entity);
  138. await SaveBomsAsync(designId, input.Boms ?? new List<ProductDesignBomInput>(), isNew: true);
  139. await SaveRoutingsAsync(designId, input.Routings ?? new List<ProductDesignRoutingInput>(), isNew: true);
  140. await _db.Ado.CommitTranAsync();
  141. return new { id = designId, billNo, message = "新增成功" };
  142. }
  143. var existing = await _designRep.GetFirstAsync(u => u.Id == input.Id!.Value)
  144. ?? throw Oops.Oh("产品设计记录不存在");
  145. designId = existing.Id;
  146. MapMaster(existing, input, user, now, isNew: false);
  147. await _designRep.UpdateAsync(existing);
  148. await SaveBomsAsync(designId, input.Boms ?? new List<ProductDesignBomInput>(), isNew: false);
  149. await SaveRoutingsAsync(designId, input.Routings ?? new List<ProductDesignRoutingInput>(), isNew: false);
  150. await _db.Ado.CommitTranAsync();
  151. return new { id = designId, billNo = existing.BillNo, message = "编辑成功" };
  152. }
  153. catch
  154. {
  155. await _db.Ado.RollbackTranAsync();
  156. throw;
  157. }
  158. }
  159. [DisplayName("删除产品设计")]
  160. [ApiDescriptionSettings(Name = "DeleteProductDesign"), HttpPost("productdesign/delete")]
  161. public async Task<object> DeleteProductDesign([FromBody] ProductDesignDeleteInput input)
  162. {
  163. var entity = await _designRep.GetFirstAsync(u => u.Id == input.Id)
  164. ?? throw Oops.Oh("产品设计记录不存在");
  165. await _db.Ado.BeginTranAsync();
  166. try
  167. {
  168. await _bomRep.DeleteAsync(u => u.ProductDesignId == entity.Id);
  169. await _routingRep.DeleteAsync(u => u.ProductDesignId == entity.Id);
  170. await _designRep.DeleteAsync(u => u.Id == entity.Id);
  171. await _db.Ado.CommitTranAsync();
  172. return new { message = "删除成功" };
  173. }
  174. catch
  175. {
  176. await _db.Ado.RollbackTranAsync();
  177. throw;
  178. }
  179. }
  180. private static ProductDesign MapMaster(ProductDesign entity, ProductDesignSaveInput input, string user, DateTime now, bool isNew)
  181. {
  182. entity.ContractNo = string.IsNullOrWhiteSpace(input.ContractNo) ? null : input.ContractNo.Trim();
  183. entity.ProductKind = input.ProductKind;
  184. entity.DesignLeadAccount = string.IsNullOrWhiteSpace(input.DesignLeadAccount) ? null : input.DesignLeadAccount.Trim();
  185. entity.DesignLeadName = string.IsNullOrWhiteSpace(input.DesignLeadName) ? null : input.DesignLeadName.Trim();
  186. entity.DrawingNo = string.IsNullOrWhiteSpace(input.DrawingNo) ? null : input.DrawingNo.Trim();
  187. entity.DrawingPlanStart = ParseOptionalDate(input.DrawingPlanStart);
  188. entity.DrawingPlanEnd = ParseOptionalDate(input.DrawingPlanEnd);
  189. entity.DrawingActualStart = ParseOptionalDate(input.DrawingActualStart);
  190. entity.DrawingActualEnd = ParseOptionalDate(input.DrawingActualEnd);
  191. entity.Applicant = string.IsNullOrWhiteSpace(input.Applicant) ? null : input.Applicant.Trim();
  192. entity.ApplyDate = ParseOptionalDate(input.ApplyDate);
  193. entity.ProductModel = string.IsNullOrWhiteSpace(input.ProductModel) ? null : input.ProductModel.Trim();
  194. entity.ItemNum = string.IsNullOrWhiteSpace(input.ItemNum) ? null : input.ItemNum.Trim();
  195. entity.Language = string.IsNullOrWhiteSpace(input.Language) ? null : input.Language.Trim();
  196. entity.Qty = null;
  197. entity.LineRemark = string.IsNullOrWhiteSpace(input.LineRemark) ? null : input.LineRemark.Trim();
  198. if (isNew)
  199. {
  200. entity.CreateUser = user;
  201. entity.CreateTime = now;
  202. entity.IsActive = 1;
  203. }
  204. else
  205. {
  206. entity.UpdateUser = user;
  207. entity.UpdateTime = now;
  208. }
  209. return entity;
  210. }
  211. private static DateTime? ParseOptionalDate(string? s)
  212. {
  213. if (string.IsNullOrWhiteSpace(s)) return null;
  214. return DateTime.TryParse(s.Trim(), out var dt) ? dt : null;
  215. }
  216. private async Task SaveBomsAsync(long designId, List<ProductDesignBomInput> items, bool isNew)
  217. {
  218. var dbRows = isNew ? new List<ProductDesignBom>()
  219. : await _bomRep.GetListAsync(u => u.ProductDesignId == designId);
  220. var dbById = dbRows.ToDictionary(u => u.Id);
  221. if (items.Any(x => !x.Id.HasValue))
  222. throw Oops.Oh("BOM 每行必须包含 Id(已保存行为正数,新行请使用负数临时 Id)");
  223. var ordered = OrderBomsForSave(items);
  224. var tempToReal = new Dictionary<long, long>();
  225. var keptIds = new HashSet<long>();
  226. var idx = 0;
  227. foreach (var d in ordered)
  228. {
  229. idx++;
  230. var seq = d.Seq ?? idx;
  231. var parentResolved = ResolveBomParentId(d.ParentBomId, tempToReal);
  232. if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing))
  233. {
  234. if (existing.ProductDesignId != designId) continue;
  235. existing.ParentBomId = parentResolved;
  236. existing.Seq = seq;
  237. existing.ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim();
  238. existing.ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim();
  239. existing.ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim();
  240. existing.Qty = null;
  241. existing.FixedLossQty = d.FixedLossQty;
  242. existing.BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim();
  243. await _bomRep.UpdateAsync(existing);
  244. keptIds.Add(existing.Id);
  245. }
  246. else if (d.Id is < 0)
  247. {
  248. var newId = YitIdHelper.NextId();
  249. tempToReal[d.Id.Value] = newId;
  250. var row = new ProductDesignBom
  251. {
  252. Id = newId,
  253. ProductDesignId = designId,
  254. ParentBomId = parentResolved,
  255. Seq = seq,
  256. ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim(),
  257. ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim(),
  258. ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim(),
  259. Qty = null,
  260. FixedLossQty = d.FixedLossQty,
  261. BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim(),
  262. };
  263. await _bomRep.InsertAsync(row);
  264. keptIds.Add(newId);
  265. }
  266. }
  267. foreach (var row in dbRows.Where(u => !keptIds.Contains(u.Id)))
  268. await _bomRep.DeleteAsync(u => u.Id == row.Id);
  269. }
  270. /// <summary>按父子顺序排列:父(含库中已有 Id)先于子;负数临时 Id 先于引用它的子行。</summary>
  271. private static List<ProductDesignBomInput> OrderBomsForSave(List<ProductDesignBomInput> items)
  272. {
  273. var withId = items.Where(x => x.Id.HasValue).ToList();
  274. var inBatch = withId.Select(x => x.Id!.Value).ToHashSet();
  275. var done = new HashSet<long>();
  276. var result = new List<ProductDesignBomInput>();
  277. var remaining = withId.ToList();
  278. bool CanTake(ProductDesignBomInput r)
  279. {
  280. var p = r.ParentBomId;
  281. if (p is null or 0) return true;
  282. if (p < 0) return done.Contains(p.Value);
  283. if (inBatch.Contains(p.Value)) return done.Contains(p.Value);
  284. return true;
  285. }
  286. while (remaining.Count > 0)
  287. {
  288. var batch = remaining.Where(CanTake).ToList();
  289. if (batch.Count == 0)
  290. throw Oops.Oh("BOM 父子关系无效(循环或缺失父节点)");
  291. foreach (var b in batch)
  292. {
  293. result.Add(b);
  294. remaining.Remove(b);
  295. if (b.Id.HasValue)
  296. done.Add(b.Id.Value);
  297. }
  298. }
  299. return result;
  300. }
  301. private static long? ResolveBomParentId(long? parentBomId, Dictionary<long, long> tempToReal)
  302. {
  303. if (parentBomId is null or 0) return null;
  304. if (parentBomId.Value < 0)
  305. {
  306. if (tempToReal.TryGetValue(parentBomId.Value, out var real))
  307. return real;
  308. throw Oops.Oh("BOM 父节点未找到,请检查父子顺序");
  309. }
  310. return parentBomId;
  311. }
  312. private async Task SaveRoutingsAsync(long designId, List<ProductDesignRoutingInput> items, bool isNew)
  313. {
  314. var dbRows = isNew ? new List<ProductDesignRouting>()
  315. : await _routingRep.GetListAsync(u => u.ProductDesignId == designId);
  316. var dbById = dbRows.ToDictionary(u => u.Id);
  317. var inputIds = new HashSet<long>(items.Where(d => d.Id is > 0).Select(d => d.Id!.Value));
  318. var idx = 0;
  319. foreach (var d in items)
  320. {
  321. idx++;
  322. var seq = d.Seq ?? idx;
  323. if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing))
  324. {
  325. if (existing.ProductDesignId != designId) continue;
  326. existing.Seq = seq;
  327. existing.OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim();
  328. existing.OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim();
  329. existing.ParentOpCode = null;
  330. existing.IsKeyProcess = d.IsKeyProcess;
  331. existing.ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim();
  332. existing.RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim();
  333. await _routingRep.UpdateAsync(existing);
  334. }
  335. else
  336. {
  337. var row = new ProductDesignRouting
  338. {
  339. Id = YitIdHelper.NextId(),
  340. ProductDesignId = designId,
  341. Seq = seq,
  342. OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim(),
  343. OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim(),
  344. ParentOpCode = null,
  345. IsKeyProcess = d.IsKeyProcess,
  346. ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim(),
  347. RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim(),
  348. };
  349. await _routingRep.InsertAsync(row);
  350. }
  351. }
  352. foreach (var toDelete in dbRows.Where(u => !inputIds.Contains(u.Id)))
  353. await _routingRep.DeleteAsync(u => u.Id == toDelete.Id);
  354. }
  355. }