ProductDesignService.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. using Yitter.IdGenerator;
  2. using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
  3. namespace Admin.NET.Plugin.AiDOP.Order;
  4. /// <summary>
  5. /// 产品设计服务(常规/非标;合同维度跟踪设计负责人、图号、BOM、工艺、图纸计划/实际时间)
  6. /// 路由前缀:/api/Order/productdesign/...
  7. /// </summary>
  8. [ApiDescriptionSettings(Order = 255, Description = "产品设计")]
  9. [Route("api/Order")]
  10. [AllowAnonymous]
  11. [NonUnify]
  12. public class ProductDesignService : IDynamicApiController, ITransient
  13. {
  14. private readonly ISqlSugarClient _db;
  15. private readonly SqlSugarRepository<ProductDesign> _designRep;
  16. private readonly SqlSugarRepository<ProductDesignBom> _bomRep;
  17. private readonly SqlSugarRepository<ProductDesignRouting> _routingRep;
  18. private readonly UserManager _userManager;
  19. public ProductDesignService(
  20. ISqlSugarClient db,
  21. SqlSugarRepository<ProductDesign> designRep,
  22. SqlSugarRepository<ProductDesignBom> bomRep,
  23. SqlSugarRepository<ProductDesignRouting> routingRep,
  24. UserManager userManager)
  25. {
  26. _db = db;
  27. _designRep = designRep;
  28. _bomRep = bomRep;
  29. _routingRep = routingRep;
  30. _userManager = userManager;
  31. }
  32. [DisplayName("获取产品设计列表")]
  33. [HttpGet("productdesign/list")]
  34. public async Task<object> GetProductDesignList([FromQuery] ProductDesignListInput input)
  35. {
  36. var tenantId = _userManager.TenantId;
  37. var q = _db.Queryable<ProductDesign>()
  38. .Where(u => u.TenantId == tenantId)
  39. .WhereIF(!string.IsNullOrWhiteSpace(input.BillNo), u => u.BillNo.Contains(input.BillNo!.Trim()))
  40. .WhereIF(!string.IsNullOrWhiteSpace(input.ContractNo), u => u.ContractNo != null && u.ContractNo.Contains(input.ContractNo!.Trim()))
  41. .WhereIF(input.ProductKind is 1 or 2, u => u.ProductKind == input.ProductKind!.Value)
  42. .WhereIF(!string.IsNullOrWhiteSpace(input.DesignLeadName), u => u.DesignLeadName != null && u.DesignLeadName.Contains(input.DesignLeadName!.Trim()));
  43. var paged = await q.OrderByDescending(u => u.Id).ToPagedListAsync(input.Page, input.PageSize);
  44. var list = paged.Items.Select(u => new
  45. {
  46. id = u.Id,
  47. billNo = u.BillNo,
  48. contractNo = u.ContractNo,
  49. productKind = u.ProductKind,
  50. designLeadAccount = u.DesignLeadAccount,
  51. designLeadName = u.DesignLeadName,
  52. drawingNo = u.DrawingNo,
  53. drawingPlanStart = u.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm"),
  54. drawingPlanEnd = u.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm"),
  55. drawingActualStart = u.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm"),
  56. drawingActualEnd = u.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm"),
  57. drawingDesignCycle = u.DrawingDesignCycle,
  58. applicant = u.Applicant,
  59. applyDate = u.ApplyDate?.ToString("yyyy-MM-dd"),
  60. productModel = u.ProductModel,
  61. itemNum = u.ItemNum,
  62. productName = u.ProductName,
  63. createTime = u.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  64. updateTime = u.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  65. }).ToList();
  66. return new { total = paged.Total, page = input.Page, pageSize = input.PageSize, list };
  67. }
  68. [DisplayName("获取产品设计详情")]
  69. [HttpGet("productdesign/{id:long}")]
  70. public async Task<object> GetProductDesignDetail(long id)
  71. {
  72. var tenantId = _userManager.TenantId;
  73. var m = await _designRep.GetFirstAsync(u => u.Id == id && u.TenantId == tenantId)
  74. ?? throw Oops.Oh("产品设计记录不存在");
  75. var boms = await _bomRep.GetListAsync(u => u.ProductDesignId == id && u.TenantId == tenantId);
  76. var routings = await _routingRep.GetListAsync(u => u.ProductDesignId == id && u.TenantId == tenantId);
  77. return new
  78. {
  79. id = m.Id,
  80. billNo = m.BillNo,
  81. contractNo = m.ContractNo,
  82. productKind = m.ProductKind,
  83. designLeadAccount = m.DesignLeadAccount,
  84. designLeadName = m.DesignLeadName,
  85. drawingNo = m.DrawingNo,
  86. drawingPlanStart = m.DrawingPlanStart?.ToString("yyyy-MM-dd HH:mm:ss"),
  87. drawingPlanEnd = m.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
  88. drawingActualStart = m.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm:ss"),
  89. drawingActualEnd = m.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
  90. drawingDesignCycle = m.DrawingDesignCycle,
  91. applicant = m.Applicant,
  92. applyDate = m.ApplyDate?.ToString("yyyy-MM-dd"),
  93. productModel = m.ProductModel,
  94. itemNum = m.ItemNum,
  95. productName = m.ProductName,
  96. language = m.Language,
  97. lineRemark = m.LineRemark,
  98. createUser = m.CreateUser,
  99. createTime = m.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  100. updateUser = m.UpdateUser,
  101. updateTime = m.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  102. boms = boms.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new
  103. {
  104. id = x.Id.ToString(),
  105. parentBomId = x.ParentBomId.HasValue ? x.ParentBomId.Value.ToString() : null,
  106. seq = x.Seq,
  107. itemNum = x.ItemNum,
  108. itemName = x.ItemName,
  109. processCode = x.ProcessCode,
  110. fixedLossQty = x.FixedLossQty,
  111. batchNo = x.BatchNo,
  112. }),
  113. routings = routings.OrderBy(x => x.Seq).ThenBy(x => x.Id).Select(x => new
  114. {
  115. id = x.Id,
  116. seq = x.Seq,
  117. opName = x.OpName,
  118. opCode = x.OpCode,
  119. isKeyProcess = x.IsKeyProcess,
  120. productionLine = x.ProductionLine,
  121. routeCode = x.RouteCode,
  122. }),
  123. };
  124. }
  125. [DisplayName("保存产品设计")]
  126. [ApiDescriptionSettings(Name = "SaveProductDesign"), HttpPost("productdesign/save")]
  127. public async Task<object> SaveProductDesign([FromBody] ProductDesignSaveInput input)
  128. {
  129. if (input.ProductKind is not (1 or 2))
  130. throw Oops.Oh("产品类型无效,请选择常规产品或非标产品");
  131. // 产品编码必填
  132. if (string.IsNullOrWhiteSpace(input.ItemNum))
  133. throw Oops.Oh("产品编码不能为空");
  134. // BOM 子表至少一条
  135. if (input.Boms == null || input.Boms.Count == 0)
  136. throw Oops.Oh("制造 BOM 至少需要一条数据");
  137. // 工艺子表至少一条
  138. if (input.Routings == null || input.Routings.Count == 0)
  139. throw Oops.Oh("工艺路线至少需要一条数据");
  140. // BOM 物料编码必填
  141. if (input.Boms.Any(b => string.IsNullOrWhiteSpace(b.ItemNum)))
  142. throw Oops.Oh("制造 BOM 中物料编码不能为空");
  143. // 工艺工序代码必填
  144. if (input.Routings.Any(r => string.IsNullOrWhiteSpace(r.OpCode)))
  145. throw Oops.Oh("工艺路线中工序代码不能为空");
  146. var now = DateTime.Now;
  147. var user = _userManager.Account ?? "system";
  148. var tenantId = _userManager.TenantId;
  149. await _db.Ado.BeginTranAsync();
  150. try
  151. {
  152. long designId;
  153. if (input.Id is null or 0)
  154. {
  155. var month = now.ToString("yyyyMM");
  156. var maxSeq = await _db.Ado.GetIntAsync(
  157. $"SELECT IFNULL(MAX(CAST(SUBSTRING(BillNo, 9) AS UNSIGNED)), 0) FROM ado_product_design WHERE BillNo LIKE 'PD{month}%' AND tenant_id = @TenantId",
  158. new SugarParameter("@TenantId", tenantId));
  159. var billNo = $"PD{month}{(maxSeq + 1):D4}";
  160. designId = YitIdHelper.NextId();
  161. var entity = MapMaster(new ProductDesign { Id = designId, BillNo = billNo, TenantId = tenantId }, input, user, now, isNew: true);
  162. await _designRep.InsertAsync(entity);
  163. await SaveBomsAsync(designId, tenantId, input.Boms ?? new List<ProductDesignBomInput>(), isNew: true);
  164. await SaveRoutingsAsync(designId, tenantId, input.Routings ?? new List<ProductDesignRoutingInput>(), isNew: true);
  165. await _db.Ado.CommitTranAsync();
  166. return new { id = designId, billNo, message = "新增成功" };
  167. }
  168. var existing = await _designRep.GetFirstAsync(u => u.Id == input.Id!.Value && u.TenantId == tenantId)
  169. ?? throw Oops.Oh("产品设计记录不存在");
  170. designId = existing.Id;
  171. MapMaster(existing, input, user, now, isNew: false);
  172. await _designRep.UpdateAsync(existing);
  173. await SaveBomsAsync(designId, tenantId, input.Boms ?? new List<ProductDesignBomInput>(), isNew: false);
  174. await SaveRoutingsAsync(designId, tenantId, input.Routings ?? new List<ProductDesignRoutingInput>(), isNew: false);
  175. await _db.Ado.CommitTranAsync();
  176. return new { id = designId, billNo = existing.BillNo, message = "编辑成功" };
  177. }
  178. catch
  179. {
  180. await _db.Ado.RollbackTranAsync();
  181. throw;
  182. }
  183. }
  184. [DisplayName("删除产品设计")]
  185. [ApiDescriptionSettings(Name = "DeleteProductDesign"), HttpPost("productdesign/delete")]
  186. public async Task<object> DeleteProductDesign([FromBody] ProductDesignDeleteInput input)
  187. {
  188. var tenantId = _userManager.TenantId;
  189. var entity = await _designRep.GetFirstAsync(u => u.Id == input.Id && u.TenantId == tenantId)
  190. ?? throw Oops.Oh("产品设计记录不存在");
  191. await _db.Ado.BeginTranAsync();
  192. try
  193. {
  194. await _bomRep.DeleteAsync(u => u.ProductDesignId == entity.Id && u.TenantId == tenantId);
  195. await _routingRep.DeleteAsync(u => u.ProductDesignId == entity.Id && u.TenantId == tenantId);
  196. await _designRep.DeleteAsync(u => u.Id == entity.Id);
  197. await _db.Ado.CommitTranAsync();
  198. return new { message = "删除成功" };
  199. }
  200. catch
  201. {
  202. await _db.Ado.RollbackTranAsync();
  203. throw;
  204. }
  205. }
  206. [DisplayName("根据产品编码获取BOM和工艺")]
  207. [HttpGet("productdesign/bom-routing")]
  208. public async Task<BomAndRoutingOutput> GetBomAndRouting([FromQuery] string itemNum)
  209. {
  210. if (string.IsNullOrWhiteSpace(itemNum))
  211. return new BomAndRoutingOutput();
  212. // 🔴 租户硬门:本方法的两段裸 SQL 走 _db.Ado.SqlQueryAsync,**完全绕过** SqlSugar 的
  213. // QueryFilter(后者只作用于 Queryable<T>),必须自己带 tenant_id。
  214. // 且本控制器是 [AllowAnonymous],匿名请求下 SqlSugarSetup 的 ITenantIdFilter
  215. // 根本不会注册(它要求 JWT 里有 TenantId claim),连兜底都没有。
  216. // tenantId 为 0(匿名/无 claim)时下面所有谓词都匹配不到行 —— fail-closed,符合预期。
  217. var tenantId = _userManager.TenantId;
  218. // 查询图纸设计周期
  219. var drawingDesignCycle = await _db.Queryable<AdoS0ItemMaster>()
  220. .Where(x => x.TenantId == tenantId && x.ItemNum == itemNum && x.IsActive == true)
  221. .Select(x => x.DrawingDesign)
  222. .FirstAsync();
  223. // 🔴 七处 tenant_id 谓词缺一不可(2026-09-08 实测,逐条见下)。
  224. //
  225. // **只在最外层 WHERE 过滤是无效的**,有三层原因:
  226. // ① 语法上不成立 —— CTE 的列清单里根本没有 tenant_id;
  227. // ② 即便把 tenant_id 加进 CTE 列再只在外层过滤,仍会残留"血统不明的自家行" ——
  228. // 实测料号 3124C0015:外层过滤版比正确版多出 15/15/150 行,这些行自身的
  229. // tenant_id 等于查询租户、外层删不掉,但它们是靠"经过外租户的边"才被走到的节点
  230. // (ItemMaster 有 663 个跨租户重码,递归中间层一旦串到别人的图上就会这样回落);
  231. // ③ 最外层的 LEFT JOIN ItemMaster 本身就是独立放大器 —— 实测 452 行 CTE 经它膨胀到 1120 行。
  232. //
  233. // ⚠️ ④⑤⑦ 三处**必须放在 ON 而不是 WHERE**:放 WHERE 会把无匹配的 NULL 行滤掉,
  234. // LEFT JOIN 退化成 INNER JOIN,没有物料主数据/产线的 BOM 行会整行消失。
  235. var bomSql = @"
  236. WITH RECURSIVE temp(ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed) AS (
  237. SELECT ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed
  238. FROM ProductStructureMaster WHERE ParentItem=@itemNum AND tenant_id=@tenantId
  239. UNION ALL
  240. SELECT c.ParentItem,c.ComponentItem,c.op,
  241. CAST(c.qty+(c.qty*(c.Scrap+c.QtyExchd)/100) AS DECIMAL(15,8)) AS qty,
  242. c.StructureType,c.QtyConsumed
  243. FROM ProductStructureMaster c
  244. INNER JOIN temp p ON c.ParentItem=p.ComponentItem
  245. INNER JOIN ItemMaster parentIm ON c.ParentItem=parentIm.ItemNum AND parentIm.PurMfg<>'P'
  246. AND parentIm.tenant_id=@tenantId
  247. WHERE c.tenant_id=@tenantId
  248. )
  249. SELECT psm.ParentItem,psm.ComponentItem AS ItemNum,im.Descr AS ItemName,
  250. CASE WHEN IFNULL(pso.Op,0)=0 THEN psm.Op ELSE pso.Op END AS Op,
  251. psm.qty AS Qty,psm.StructureType,im.EMTType AS EmtType,psm.QtyConsumed
  252. FROM temp psm
  253. LEFT JOIN ItemMaster im ON psm.ComponentItem=im.ItemNum
  254. AND im.tenant_id=@tenantId
  255. LEFT JOIN ProductStructureOp pso ON pso.ParentItem=psm.ParentItem
  256. AND pso.ComponentItem=psm.ComponentItem AND pso.ProductItem=@itemNum
  257. AND pso.tenant_id=@tenantId";
  258. var routingSql = @"
  259. SELECT r.Descr,r.Op,r.ParentOp,CAST(r.MilestoneOp AS CHAR(5)) AS MilestoneOp,p.Line,r.RouteCode
  260. FROM RoutingOpDetail as r left join ProdLineDetail as p on r.RoutingCode=p.Part and r.Op=p.Op
  261. and p.tenant_id=@tenantId
  262. WHERE r.RoutingCode=@itemNum AND r.tenant_id=@tenantId ORDER BY r.Op";
  263. var boms = await _db.Ado.SqlQueryAsync<BomQueryRow>(bomSql, new { itemNum, tenantId });
  264. var routings = await _db.Ado.SqlQueryAsync<RoutingQueryRow>(routingSql, new { itemNum, tenantId });
  265. return new BomAndRoutingOutput
  266. {
  267. Boms = boms,
  268. Routings = routings,
  269. DrawingDesignCycle = drawingDesignCycle
  270. };
  271. }
  272. private static ProductDesign MapMaster(ProductDesign entity, ProductDesignSaveInput input, string user, DateTime now, bool isNew)
  273. {
  274. entity.ContractNo = string.IsNullOrWhiteSpace(input.ContractNo) ? null : input.ContractNo.Trim();
  275. entity.ProductKind = input.ProductKind;
  276. entity.DesignLeadAccount = string.IsNullOrWhiteSpace(input.DesignLeadAccount) ? null : input.DesignLeadAccount.Trim();
  277. entity.DesignLeadName = string.IsNullOrWhiteSpace(input.DesignLeadName) ? null : input.DesignLeadName.Trim();
  278. entity.DrawingNo = string.IsNullOrWhiteSpace(input.DrawingNo) ? null : input.DrawingNo.Trim();
  279. entity.DrawingPlanStart = ParseOptionalDate(input.DrawingPlanStart);
  280. entity.DrawingPlanEnd = ParseOptionalDate(input.DrawingPlanEnd);
  281. entity.DrawingActualStart = ParseOptionalDate(input.DrawingActualStart);
  282. entity.DrawingActualEnd = ParseOptionalDate(input.DrawingActualEnd);
  283. entity.DrawingDesignCycle = input.DrawingDesignCycle;
  284. entity.Applicant = string.IsNullOrWhiteSpace(input.Applicant) ? null : input.Applicant.Trim();
  285. entity.ApplyDate = ParseOptionalDate(input.ApplyDate);
  286. entity.ProductModel = string.IsNullOrWhiteSpace(input.ProductModel) ? null : input.ProductModel.Trim();
  287. entity.ItemNum = string.IsNullOrWhiteSpace(input.ItemNum) ? null : input.ItemNum.Trim();
  288. entity.ProductName = string.IsNullOrWhiteSpace(input.ProductName) ? null : input.ProductName.Trim();
  289. entity.Language = string.IsNullOrWhiteSpace(input.Language) ? null : input.Language.Trim();
  290. entity.Qty = null;
  291. entity.LineRemark = string.IsNullOrWhiteSpace(input.LineRemark) ? null : input.LineRemark.Trim();
  292. if (isNew)
  293. {
  294. entity.CreateUser = user;
  295. entity.CreateTime = now;
  296. entity.IsActive = 1;
  297. }
  298. else
  299. {
  300. entity.UpdateUser = user;
  301. entity.UpdateTime = now;
  302. }
  303. return entity;
  304. }
  305. private static DateTime? ParseOptionalDate(string? s)
  306. {
  307. if (string.IsNullOrWhiteSpace(s)) return null;
  308. return DateTime.TryParse(s.Trim(), out var dt) ? dt : null;
  309. }
  310. private async Task SaveBomsAsync(long designId, long tenantId, List<ProductDesignBomInput> items, bool isNew)
  311. {
  312. var dbRows = isNew ? new List<ProductDesignBom>()
  313. : await _bomRep.GetListAsync(u => u.ProductDesignId == designId && u.TenantId == tenantId);
  314. var dbById = dbRows.ToDictionary(u => u.Id);
  315. if (items.Any(x => !x.Id.HasValue))
  316. throw Oops.Oh("BOM 每行必须包含 Id(已保存行为正数,新行请使用负数临时 Id)");
  317. var ordered = OrderBomsForSave(items);
  318. var tempToReal = new Dictionary<long, long>();
  319. var keptIds = new HashSet<long>();
  320. var idx = 0;
  321. foreach (var d in ordered)
  322. {
  323. idx++;
  324. var seq = d.Seq ?? idx;
  325. var parentResolved = ResolveBomParentId(d.ParentBomId, tempToReal);
  326. if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing))
  327. {
  328. if (existing.ProductDesignId != designId) continue;
  329. existing.ParentBomId = parentResolved;
  330. existing.Seq = seq;
  331. existing.ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim();
  332. existing.ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim();
  333. existing.ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim();
  334. existing.Qty = d.Qty;
  335. existing.FixedLossQty = d.FixedLossQty;
  336. existing.BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim();
  337. await _bomRep.UpdateAsync(existing);
  338. keptIds.Add(existing.Id);
  339. }
  340. else if (d.Id is < 0)
  341. {
  342. var newId = YitIdHelper.NextId();
  343. tempToReal[d.Id.Value] = newId;
  344. var row = new ProductDesignBom
  345. {
  346. Id = newId,
  347. ProductDesignId = designId,
  348. TenantId = tenantId,
  349. ParentBomId = parentResolved,
  350. Seq = seq,
  351. ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim(),
  352. ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim(),
  353. ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim(),
  354. Qty = d.Qty,
  355. FixedLossQty = d.FixedLossQty,
  356. BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim(),
  357. };
  358. await _bomRep.InsertAsync(row);
  359. keptIds.Add(newId);
  360. }
  361. }
  362. foreach (var row in dbRows.Where(u => !keptIds.Contains(u.Id)))
  363. await _bomRep.DeleteAsync(u => u.Id == row.Id && u.TenantId == tenantId);
  364. }
  365. /// <summary>按父子顺序排列:父(含库中已有 Id)先于子;负数临时 Id 先于引用它的子行。</summary>
  366. private static List<ProductDesignBomInput> OrderBomsForSave(List<ProductDesignBomInput> items)
  367. {
  368. var withId = items.Where(x => x.Id.HasValue).ToList();
  369. var inBatch = withId.Select(x => x.Id!.Value).ToHashSet();
  370. var done = new HashSet<long>();
  371. var result = new List<ProductDesignBomInput>();
  372. var remaining = withId.ToList();
  373. bool CanTake(ProductDesignBomInput r)
  374. {
  375. var p = r.ParentBomId;
  376. if (p is null or 0) return true;
  377. if (p < 0) return done.Contains(p.Value);
  378. if (inBatch.Contains(p.Value)) return done.Contains(p.Value);
  379. return true;
  380. }
  381. while (remaining.Count > 0)
  382. {
  383. var batch = remaining.Where(CanTake).ToList();
  384. if (batch.Count == 0)
  385. throw Oops.Oh("BOM 父子关系无效(循环或缺失父节点)");
  386. foreach (var b in batch)
  387. {
  388. result.Add(b);
  389. remaining.Remove(b);
  390. if (b.Id.HasValue)
  391. done.Add(b.Id.Value);
  392. }
  393. }
  394. return result;
  395. }
  396. private static long? ResolveBomParentId(long? parentBomId, Dictionary<long, long> tempToReal)
  397. {
  398. if (parentBomId is null or 0) return null;
  399. if (parentBomId.Value < 0)
  400. {
  401. if (tempToReal.TryGetValue(parentBomId.Value, out var real))
  402. return real;
  403. throw Oops.Oh("BOM 父节点未找到,请检查父子顺序");
  404. }
  405. return parentBomId;
  406. }
  407. private async Task SaveRoutingsAsync(long designId, long tenantId, List<ProductDesignRoutingInput> items, bool isNew)
  408. {
  409. var dbRows = isNew ? new List<ProductDesignRouting>()
  410. : await _routingRep.GetListAsync(u => u.ProductDesignId == designId && u.TenantId == tenantId);
  411. var dbById = dbRows.ToDictionary(u => u.Id);
  412. var inputIds = new HashSet<long>(items.Where(d => d.Id is > 0).Select(d => d.Id!.Value));
  413. var idx = 0;
  414. foreach (var d in items)
  415. {
  416. idx++;
  417. var seq = d.Seq ?? idx;
  418. if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing))
  419. {
  420. if (existing.ProductDesignId != designId) continue;
  421. existing.Seq = seq;
  422. existing.OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim();
  423. existing.OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim();
  424. existing.ParentOpCode = null;
  425. existing.IsKeyProcess = d.IsKeyProcess;
  426. existing.ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim();
  427. existing.RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim();
  428. await _routingRep.UpdateAsync(existing);
  429. }
  430. else
  431. {
  432. var row = new ProductDesignRouting
  433. {
  434. Id = YitIdHelper.NextId(),
  435. ProductDesignId = designId,
  436. TenantId = tenantId,
  437. Seq = seq,
  438. OpName = string.IsNullOrWhiteSpace(d.OpName) ? null : d.OpName.Trim(),
  439. OpCode = string.IsNullOrWhiteSpace(d.OpCode) ? null : d.OpCode.Trim(),
  440. ParentOpCode = null,
  441. IsKeyProcess = d.IsKeyProcess,
  442. ProductionLine = string.IsNullOrWhiteSpace(d.ProductionLine) ? null : d.ProductionLine.Trim(),
  443. RouteCode = string.IsNullOrWhiteSpace(d.RouteCode) ? null : d.RouteCode.Trim(),
  444. };
  445. await _routingRep.InsertAsync(row);
  446. }
  447. }
  448. foreach (var toDelete in dbRows.Where(u => !inputIds.Contains(u.Id)))
  449. await _routingRep.DeleteAsync(u => u.Id == toDelete.Id && u.TenantId == tenantId);
  450. }
  451. /// <summary>获取合同下拉选项</summary>
  452. [DisplayName("获取合同下拉选项")]
  453. [HttpGet("productdesign/contract-options")]
  454. public async Task<List<object>> GetContractOptions([FromQuery] string? keyword)
  455. {
  456. var tenantId = _userManager.TenantId;
  457. var sql = string.IsNullOrWhiteSpace(keyword)
  458. ? "SELECT BillNo, CONCAT(BillNo, ' | ', IFNULL(Title,'')) AS Label FROM ado_contract_review WHERE tenant_id = @tenantId ORDER BY RecID DESC LIMIT 50"
  459. : "SELECT BillNo, CONCAT(BillNo, ' | ', IFNULL(Title,'')) AS Label FROM ado_contract_review WHERE tenant_id = @tenantId AND (BillNo LIKE @kw OR Title LIKE @kw) ORDER BY RecID DESC LIMIT 50";
  460. var rows = await _db.Ado.SqlQueryAsync<ContractOptionRow>(sql,
  461. new { tenantId, kw = string.IsNullOrWhiteSpace(keyword) ? null : $"%{keyword.Trim()}%" });
  462. return rows.Select(r => (object)new { value = r.BillNo, label = r.Label }).ToList();
  463. }
  464. /// <summary>获取用户下拉选项</summary>
  465. [DisplayName("获取用户下拉选项")]
  466. [HttpGet("productdesign/user-options")]
  467. public async Task<List<object>> GetUserOptions([FromQuery] string? keyword)
  468. {
  469. var tenantId = _userManager.TenantId;
  470. var sql = string.IsNullOrWhiteSpace(keyword)
  471. ? "SELECT Id, Account, RealName FROM SysUser WHERE TenantId = @tenantId AND Status = 1 ORDER BY OrderNo LIMIT 200"
  472. : "SELECT Id, Account, RealName FROM SysUser WHERE TenantId = @tenantId AND Status = 1 AND (Account LIKE @kw OR RealName LIKE @kw) ORDER BY OrderNo LIMIT 200";
  473. var rows = await _db.Ado.SqlQueryAsync<UserOptionRow>(sql,
  474. new { tenantId, kw = string.IsNullOrWhiteSpace(keyword) ? null : $"%{keyword.Trim()}%" });
  475. return rows.Select(r => (object)new { value = r.Account, label = $"{r.RealName} ({r.Account})" }).ToList();
  476. }
  477. /// <summary>获取产线下拉选项</summary>
  478. [DisplayName("获取产线下拉选项")]
  479. [HttpGet("productdesign/line-options")]
  480. public async Task<List<object>> GetLineOptions([FromQuery] string? keyword)
  481. {
  482. var tenantId = _userManager.TenantId;
  483. var sql = string.IsNullOrWhiteSpace(keyword)
  484. ? "SELECT Line, `Describe` FROM LineMaster WHERE tenant_id = @tenantId AND IsActive = 1 ORDER BY Line LIMIT 200"
  485. : "SELECT Line, `Describe` FROM LineMaster WHERE tenant_id = @tenantId AND IsActive = 1 AND (Line LIKE @kw OR `Describe` LIKE @kw) ORDER BY Line LIMIT 200";
  486. var rows = await _db.Ado.SqlQueryAsync<LineOptionRow>(sql,
  487. new { tenantId, kw = string.IsNullOrWhiteSpace(keyword) ? null : $"%{keyword.Trim()}%" });
  488. return rows.Select(r => (object)new { value = r.Line, label = $"{r.Line} | {r.Describe}" }).ToList();
  489. }
  490. private sealed class ContractOptionRow
  491. {
  492. public string BillNo { get; set; } = string.Empty;
  493. public string Label { get; set; } = string.Empty;
  494. }
  495. private sealed class UserOptionRow
  496. {
  497. public long Id { get; set; }
  498. public string Account { get; set; } = string.Empty;
  499. public string RealName { get; set; } = string.Empty;
  500. }
  501. private sealed class LineOptionRow
  502. {
  503. public string Line { get; set; } = string.Empty;
  504. public string Describe { get; set; } = string.Empty;
  505. }
  506. }