WorkOrderBomUploadService.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. using System.Globalization;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Microsoft.Extensions.Logging;
  4. using Microsoft.Extensions.Options;
  5. using Yitter.IdGenerator;
  6. namespace Admin.NET.Plugin.AiDOP.WorkOrder;
  7. /// <summary>
  8. /// 工单 BOM 上传服务 — 从 Excel 解析结果按(订单号+父物料号)匹配未下达工单,
  9. /// 批量写入 ItemMaster + WorkOrdDetail。
  10. /// 路由前缀:/api/WorkOrder/dispatch/bom/...
  11. ///
  12. /// 【自动建单开关】当 <see cref="AidopBomUploadOptions.AutoCreateWhenUnmatched"/> = true(测试阶段)时,
  13. /// 匹配不到订单/工单的组会自动创建销售订单(crm_seorder)与工单(WorkOrdMaster+mes_morder)后继续写入;
  14. /// false(生产阶段,缺省)时只返回未匹配提示,不写入任何数据。
  15. ///
  16. /// 【租户安全边界】需认证访问;租户一律经
  17. /// <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析。
  18. /// </summary>
  19. [ApiDescriptionSettings(Order = 258, Description = "工单BOM上传")]
  20. [Route("api/WorkOrder")]
  21. [NonUnify]
  22. public class WorkOrderBomUploadService : IDynamicApiController, ITransient
  23. {
  24. private readonly ISqlSugarClient _db;
  25. private readonly UserManager _userManager;
  26. private readonly ILogger<WorkOrderBomUploadService> _logger;
  27. private readonly IOptionsMonitor<AidopBomUploadOptions> _bomUploadOpt;
  28. public WorkOrderBomUploadService(
  29. ISqlSugarClient db,
  30. UserManager userManager,
  31. ILogger<WorkOrderBomUploadService> logger,
  32. IOptionsMonitor<AidopBomUploadOptions> bomUploadOpt)
  33. {
  34. _db = db;
  35. _userManager = userManager;
  36. _logger = logger;
  37. _bomUploadOpt = bomUploadOpt;
  38. }
  39. private long ResolveTenantId() => AidopTenantScope.ResolveOrThrow(_userManager);
  40. // ══════════════════════════════════════════════════════════════
  41. // POST /api/WorkOrder/dispatch/bom/check-materials
  42. // ══════════════════════════════════════════════════════════════
  43. /// <summary>校验 BOM 子物料是否在 ItemMaster 中存在。</summary>
  44. [DisplayName("BOM物料存在性校验")]
  45. [ApiDescriptionSettings(Name = "BomCheckMaterials"), HttpPost("dispatch/bom/check-materials")]
  46. public async Task<BomCheckResult> CheckMaterials([FromBody] BomCheckMaterialsInput input)
  47. {
  48. var tid = ResolveTenantId();
  49. if (input.Items == null || input.Items.Count == 0)
  50. throw Oops.Oh("BOM 物料列表为空");
  51. var itemNums = input.Items
  52. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum))
  53. .Select(x => x.ItemNum.Trim())
  54. .Distinct()
  55. .ToList();
  56. if (itemNums.Count == 0)
  57. throw Oops.Oh("BOM 中无有效的物料编码");
  58. // 查询本租户下已存在的物料编码
  59. var existingSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  60. var pars = new List<SugarParameter> { new("@TenantId", tid) };
  61. for (var i = 0; i < itemNums.Count; i++)
  62. pars.Add(new SugarParameter($"@IN{i}", itemNums[i]));
  63. var inClause = string.Join(", ", Enumerable.Range(0, itemNums.Count).Select(i => $"@IN{i}"));
  64. var existing = await _db.Ado.SqlQueryAsync<string>(
  65. $"""
  66. SELECT DISTINCT ItemNum FROM ItemMaster
  67. WHERE tenant_id = @TenantId AND ItemNum IN ({inClause})
  68. """,
  69. pars);
  70. foreach (var e in existing)
  71. existingSet.Add(e);
  72. return new BomCheckResult
  73. {
  74. Existing = existingSet.ToList(),
  75. Missing = input.Items
  76. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum) && !existingSet.Contains(x.ItemNum.Trim()))
  77. .GroupBy(x => x.ItemNum.Trim())
  78. .Select(g => g.First())
  79. .ToList()
  80. };
  81. }
  82. // ══════════════════════════════════════════════════════════════
  83. // POST /api/WorkOrder/dispatch/bom/upload
  84. // ══════════════════════════════════════════════════════════════
  85. /// <summary>
  86. /// 写入 BOM:按(订单号+父物料号)匹配未下达工单,可选新建 ItemMaster 缺失物料,
  87. /// 然后 Upsert WorkOrdDetail。
  88. /// </summary>
  89. [DisplayName("BOM写入工单物料明细")]
  90. [ApiDescriptionSettings(Name = "BomUpload"), HttpPost("dispatch/bom/upload")]
  91. public async Task<BomUploadResult> Upload([FromBody] BomUploadInput input)
  92. {
  93. var tid = ResolveTenantId();
  94. if (input.Items == null || input.Items.Count == 0)
  95. throw Oops.Oh("BOM 物料列表为空");
  96. var account = _userManager.Account ?? "system";
  97. var factoryRefId = await ResolveFactoryRefIdAsync(tid);
  98. // 1) 查找哪些子物料需要新建(ItemMaster 存在性检查)
  99. var itemNums = input.Items
  100. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum))
  101. .Select(x => x.ItemNum.Trim())
  102. .Distinct()
  103. .ToList();
  104. var existingSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  105. if (itemNums.Count > 0)
  106. {
  107. var pars = new List<SugarParameter> { new("@TenantId", tid) };
  108. for (var i = 0; i < itemNums.Count; i++)
  109. pars.Add(new SugarParameter($"@IN{i}", itemNums[i]));
  110. var inClause = string.Join(", ", Enumerable.Range(0, itemNums.Count).Select(i => $"@IN{i}"));
  111. var existing = await _db.Ado.SqlQueryAsync<string>(
  112. $"SELECT DISTINCT ItemNum FROM ItemMaster WHERE tenant_id = @TenantId AND ItemNum IN ({inClause})",
  113. pars);
  114. foreach (var e in existing)
  115. existingSet.Add(e);
  116. }
  117. var newItemsCount = 0;
  118. // 2) 按(SalesOrderNo, ParentItemNum)分组 → 每组对应一个工单
  119. var groups = input.Items
  120. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum)
  121. && !string.IsNullOrWhiteSpace(x.SalesOrderNo)
  122. && !string.IsNullOrWhiteSpace(x.ParentItemNum))
  123. .GroupBy(x => (SalesOrderNo: x.SalesOrderNo!.Trim(), ParentItemNum: x.ParentItemNum!.Trim()))
  124. .ToList();
  125. // 没有有效分组 key 的行视为无法匹配
  126. var unmatched = new List<string>();
  127. var noKeyItems = input.Items
  128. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum)
  129. && (string.IsNullOrWhiteSpace(x.SalesOrderNo) || string.IsNullOrWhiteSpace(x.ParentItemNum)))
  130. .ToList();
  131. if (noKeyItems.Count > 0)
  132. unmatched.Add($"缺少订单号或父物料号:{noKeyItems.Count} 行");
  133. // 3) 预匹配:在写入之前先将所有分组匹配到工单,有未匹配则整体中止
  134. // 开关打开时,未匹配组转入 pendingCreate(事务内自动创建订单+工单后继续写入)
  135. var autoCreateWhenUnmatched = _bomUploadOpt.CurrentValue.AutoCreateWhenUnmatched;
  136. var matchedGroups = new List<(IGrouping<(string SalesOrderNo, string ParentItemNum), BomItemDto> Grp, WoInfo Wo)>();
  137. var pendingCreate = new List<(IGrouping<(string SalesOrderNo, string ParentItemNum), BomItemDto> Grp, string ContractNo, string ParentDescr)>();
  138. foreach (var grp in groups)
  139. {
  140. var woRow = await _db.Ado.SqlQuerySingleAsync<WoInfo>(
  141. """
  142. SELECT RecID AS RecId, WorkOrd, `Domain`, LOWER(TRIM(IFNULL(Status,''))) AS Status
  143. FROM WorkOrdMaster
  144. WHERE tenant_id = @TenantId
  145. AND TRIM(IFNULL(SalesJob,'')) = @SalesOrderNo
  146. AND TRIM(IFNULL(ItemNum,'')) = @ParentItemNum
  147. AND LOWER(TRIM(IFNULL(Status,''))) = 'p'
  148. LIMIT 1
  149. """,
  150. new SugarParameter("@TenantId", tid),
  151. new SugarParameter("@SalesOrderNo", grp.Key.SalesOrderNo),
  152. new SugarParameter("@ParentItemNum", grp.Key.ParentItemNum));
  153. if (woRow == null)
  154. {
  155. if (autoCreateWhenUnmatched)
  156. {
  157. // 提取组内首个非空的合同号/父物料描述,用于自动建单
  158. var contractNo = grp.Select(x => x.ContractNo).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)) ?? string.Empty;
  159. var parentDescr = grp.Select(x => x.ParentDescr).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)) ?? string.Empty;
  160. pendingCreate.Add((grp, contractNo, parentDescr));
  161. }
  162. else
  163. {
  164. unmatched.Add($"订单[{grp.Key.SalesOrderNo}] + 物料[{grp.Key.ParentItemNum}]");
  165. }
  166. }
  167. else
  168. {
  169. matchedGroups.Add((grp, woRow));
  170. }
  171. }
  172. // 有未匹配组(且开关关闭无法自动建单)→ 整体中止,不写入任何数据
  173. if (unmatched.Count > 0)
  174. {
  175. return new BomUploadResult
  176. {
  177. Inserted = 0, Updated = 0, NewItems = 0, Total = input.Items.Count,
  178. MatchedWorkOrders = 0, Unmatched = unmatched
  179. };
  180. }
  181. await _db.Ado.BeginTranAsync();
  182. try
  183. {
  184. // 3.5) 自动建单:开关打开时,为未匹配组创建销售订单+工单,纳入写入队列
  185. var createdOrders = 0;
  186. var createdWorkOrders = 0;
  187. foreach (var pc in pendingCreate)
  188. {
  189. var (woRow, orderCreated) = await EnsureOrderAndWorkOrderAsync(
  190. tid, pc.Grp.Key.SalesOrderNo, pc.Grp.Key.ParentItemNum,
  191. pc.ContractNo, pc.ParentDescr, factoryRefId, account);
  192. if (orderCreated) createdOrders++;
  193. createdWorkOrders++;
  194. matchedGroups.Add((pc.Grp, woRow));
  195. }
  196. // 4) 创建缺失物料到 ItemMaster
  197. if (input.CreateMissingItems)
  198. {
  199. var missingItems = input.Items
  200. .Where(x => !string.IsNullOrWhiteSpace(x.ItemNum) && !existingSet.Contains(x.ItemNum.Trim()))
  201. .GroupBy(x => x.ItemNum.Trim())
  202. .Select(g => g.First())
  203. .ToList();
  204. var now = DateTime.Now;
  205. // 获取一个默认 Domain(取第一个匹配工单的,或兜底 8010)
  206. var defaultDomain = "8010";
  207. foreach (var item in missingItems)
  208. {
  209. var itemNum = item.ItemNum.Trim();
  210. await _db.Ado.ExecuteCommandAsync(
  211. """
  212. INSERT IGNORE INTO ItemMaster (
  213. tenant_id, ItemNum, Descr, Descr1, UM,
  214. Status, IsActive, Domain, factory_ref_id,
  215. CreateTime, CreateUser, UpdateTime, UpdateUser
  216. ) VALUES (
  217. @TenantId, @ItemNum, @Descr, @Descr1, @UM,
  218. '', 1, @Domain, @FactoryRefId,
  219. @Now, @User, @Now, @User
  220. )
  221. """,
  222. new SugarParameter("@TenantId", tid),
  223. new SugarParameter("@ItemNum", itemNum),
  224. new SugarParameter("@Descr", item.Descr ?? string.Empty),
  225. new SugarParameter("@Descr1", item.Descr1 ?? (object)DBNull.Value),
  226. new SugarParameter("@UM", item.UM ?? (object)DBNull.Value),
  227. new SugarParameter("@Domain", defaultDomain),
  228. new SugarParameter("@FactoryRefId", factoryRefId),
  229. new SugarParameter("@Now", now),
  230. new SugarParameter("@User", account));
  231. newItemsCount++;
  232. existingSet.Add(itemNum);
  233. }
  234. }
  235. // 5) 逐组写入 WorkOrdDetail(已预匹配,无需再查)
  236. var inserted = 0;
  237. var updated = 0;
  238. var matchedWorkOrders = 0;
  239. foreach (var (grp, woRow) in matchedGroups)
  240. {
  241. var domain = string.IsNullOrWhiteSpace(woRow.Domain) ? "8010" : woRow.Domain;
  242. // 加载该工单已有的 WorkOrdDetail
  243. var existingDetails = await _db.Ado.SqlQueryAsync<ExistingDetailRow>(
  244. """
  245. SELECT RecID, ItemNum, QtyRequired
  246. FROM WorkOrdDetail
  247. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND IFNULL(IsActive, 0) = 1
  248. """,
  249. new SugarParameter("@TenantId", tid),
  250. new SugarParameter("@WorkOrd", woRow.WorkOrd));
  251. var existingMap = existingDetails.ToDictionary(x => x.ItemNum, x => x, StringComparer.OrdinalIgnoreCase);
  252. var line = existingDetails.Count + 1;
  253. var now2 = DateTime.Now;
  254. // 按子物料 ItemNum 去重(同一物料累加需求数量)
  255. var groupedItems = grp
  256. .GroupBy(x => x.ItemNum.Trim())
  257. .Select(g => new
  258. {
  259. ItemNum = g.Key,
  260. Descr = g.First().Descr,
  261. Descr1 = g.First().Descr1,
  262. UM = g.First().UM,
  263. QtyRequired = g.Sum(x => x.QtyRequired),
  264. Op = g.First().Op
  265. })
  266. .ToList();
  267. foreach (var item in groupedItems)
  268. {
  269. var itemNum = item.ItemNum.Trim();
  270. if (existingMap.TryGetValue(itemNum, out var ex))
  271. {
  272. // 已有 → UPDATE QtyRequired
  273. await _db.Ado.ExecuteCommandAsync(
  274. """
  275. UPDATE WorkOrdDetail
  276. SET QtyRequired = @QtyRequired, UpdateUser = @User, UpdateTime = @Now
  277. WHERE RecID = @RecID AND tenant_id = @TenantId
  278. """,
  279. new SugarParameter("@QtyRequired", item.QtyRequired),
  280. new SugarParameter("@User", account),
  281. new SugarParameter("@Now", now2),
  282. new SugarParameter("@RecID", ex.RecID),
  283. new SugarParameter("@TenantId", tid));
  284. updated++;
  285. }
  286. else
  287. {
  288. // 新增 → INSERT
  289. await _db.Ado.ExecuteCommandAsync(
  290. """
  291. INSERT INTO WorkOrdDetail (
  292. WorkOrdMasterRecID, `Domain`, WorkOrd, LineNum, ItemNum, Op,
  293. Location, QtyRequired, QtyPosted, UM, IsActive, IsConfirm,
  294. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  295. ) VALUES (
  296. @MasterRecId, @Domain, @WorkOrd, @LineNum, @ItemNum, @Op,
  297. @Location, @QtyRequired, 0, @UM, 1, 0,
  298. @User, @Now, @User, @Now, @TenantId
  299. )
  300. """,
  301. new SugarParameter("@MasterRecId", woRow.RecId),
  302. new SugarParameter("@Domain", domain),
  303. new SugarParameter("@WorkOrd", woRow.WorkOrd),
  304. new SugarParameter("@LineNum", line),
  305. new SugarParameter("@ItemNum", itemNum),
  306. new SugarParameter("@Op", item.Op),
  307. new SugarParameter("@Location", DBNull.Value),
  308. new SugarParameter("@QtyRequired", item.QtyRequired),
  309. new SugarParameter("@UM", item.UM ?? (object)DBNull.Value),
  310. new SugarParameter("@User", account),
  311. new SugarParameter("@Now", now2),
  312. new SugarParameter("@TenantId", tid));
  313. inserted++;
  314. line++;
  315. }
  316. }
  317. matchedWorkOrders++;
  318. _logger.LogInformation(
  319. "[BomUpload] workOrd={WorkOrd} salesOrderNo={SalesOrderNo} parentItem={ParentItem} items={Items}",
  320. woRow.WorkOrd, grp.Key.SalesOrderNo, grp.Key.ParentItemNum, groupedItems.Count);
  321. }
  322. await _db.Ado.CommitTranAsync();
  323. _logger.LogInformation(
  324. "[BomUpload] tenantId={TenantId} matched={Matched} inserted={Inserted} updated={Updated} newItems={NewItems} createdOrders={CreatedOrders} createdWorkOrders={CreatedWorkOrders} unmatched={Unmatched}",
  325. tid, matchedWorkOrders, inserted, updated, newItemsCount, createdOrders, createdWorkOrders, unmatched.Count);
  326. return new BomUploadResult
  327. {
  328. Inserted = inserted,
  329. Updated = updated,
  330. NewItems = newItemsCount,
  331. Total = input.Items.Count,
  332. MatchedWorkOrders = matchedWorkOrders,
  333. Unmatched = unmatched,
  334. CreatedOrders = createdOrders,
  335. CreatedWorkOrders = createdWorkOrders
  336. };
  337. }
  338. catch
  339. {
  340. await _db.Ado.RollbackTranAsync();
  341. throw;
  342. }
  343. }
  344. /// <summary>获取租户的 factory_ref_id(取 ItemMaster 中已有的最小 factory_ref_id,缺省 1)。</summary>
  345. private async Task<long> ResolveFactoryRefIdAsync(long tenantId)
  346. {
  347. var val = await _db.Ado.GetIntAsync(
  348. """
  349. SELECT IFNULL(MIN(factory_ref_id), 1) FROM ItemMaster
  350. WHERE tenant_id = @TenantId AND factory_ref_id > 0
  351. LIMIT 1
  352. """,
  353. new SugarParameter("@TenantId", tenantId));
  354. return val <= 0 ? 1 : val;
  355. }
  356. /// <summary>
  357. /// 自动建单(测试阶段开关):销售订单不存在则创建 crm_seorder,
  358. /// 随后创建未下达工单(WorkOrdMaster Status='p' + mes_morder),返回新工单信息。
  359. /// </summary>
  360. private async Task<(WoInfo Wo, bool OrderCreated)> EnsureOrderAndWorkOrderAsync(
  361. long tenantId,
  362. string salesOrderNo,
  363. string parentItemNum,
  364. string contractNo,
  365. string parentDescr,
  366. long factoryRefId,
  367. string account)
  368. {
  369. var now = DateTime.Now;
  370. // 1) 销售订单存在性检查(已存在只建工单,不重建订单)
  371. var orderId = await _db.Ado.GetLongAsync(
  372. """
  373. SELECT Id FROM crm_seorder
  374. WHERE tenant_id = @TenantId
  375. AND TRIM(IFNULL(bill_no,'')) = @BillNo
  376. AND IFNULL(IsDeleted,0) = 0
  377. LIMIT 1
  378. """,
  379. new List<SugarParameter>
  380. {
  381. new("@TenantId", tenantId),
  382. new("@BillNo", salesOrderNo)
  383. });
  384. var orderCreated = false;
  385. if (orderId <= 0)
  386. {
  387. orderId = YitIdHelper.NextId();
  388. await _db.Ado.ExecuteCommandAsync(
  389. """
  390. INSERT INTO crm_seorder (
  391. Id, bill_no, order_type, urgent, bill_from, flowstate,
  392. create_by_name, create_time, update_by_name, update_time,
  393. tenant_id, factory_id, IsDeleted, closed
  394. ) VALUES (
  395. @Id, @BillNo, 1, 0, @BillFrom, 'BOM导入',
  396. @User, @Now, @User, @Now,
  397. @TenantId, @FactoryId, 0, 0
  398. )
  399. """,
  400. new SugarParameter("@Id", orderId),
  401. new SugarParameter("@BillNo", salesOrderNo),
  402. new SugarParameter("@BillFrom", string.IsNullOrWhiteSpace(contractNo) ? (object)DBNull.Value : contractNo.Trim()),
  403. new SugarParameter("@User", account),
  404. new SugarParameter("@Now", now),
  405. new SugarParameter("@TenantId", tenantId),
  406. new SugarParameter("@FactoryId", factoryRefId));
  407. orderCreated = true;
  408. }
  409. // 2) 生成工单号并创建未下达工单(Status='p',与 BOM 匹配口径一致)
  410. var workOrd = await GenerateWorkOrdNoAsync(tenantId);
  411. var domain = await ResolveDomainAsync(tenantId);
  412. await _db.Ado.ExecuteCommandAsync(
  413. """
  414. INSERT INTO WorkOrdMaster (
  415. `Domain`, WorkOrd, ItemNum, ItemName, Status, Typed,
  416. QtyOrded, QtyCompleted, LbrVar, OrdDate, Priority, Urgent,
  417. SalesJob, BusinessID, CreateUser, CreateTime, UpdateUser, UpdateTime,
  418. IsActive, IsConfirm, tenant_id, Remark
  419. ) VALUES (
  420. @Domain, @WorkOrd, @ItemNum, @ItemName, 'p', 'M',
  421. 1, 0, 0, @OrdDate, 0, 0,
  422. @SalesJob, @BusinessId, @User, @Now, @User, @Now,
  423. 1, 0, @TenantId, @Remark
  424. )
  425. """,
  426. new SugarParameter("@Domain", domain),
  427. new SugarParameter("@WorkOrd", workOrd),
  428. new SugarParameter("@ItemNum", parentItemNum),
  429. new SugarParameter("@ItemName", string.IsNullOrWhiteSpace(parentDescr) ? string.Empty : parentDescr.Trim()),
  430. new SugarParameter("@OrdDate", now.Date),
  431. new SugarParameter("@SalesJob", salesOrderNo),
  432. new SugarParameter("@BusinessId", orderId),
  433. new SugarParameter("@User", account),
  434. new SugarParameter("@Now", now),
  435. new SugarParameter("@TenantId", tenantId),
  436. new SugarParameter("@Remark", "BOM上传自动建单"));
  437. var recId = await _db.Ado.GetLongAsync(
  438. "SELECT RecID FROM WorkOrdMaster WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd LIMIT 1",
  439. new List<SugarParameter>
  440. {
  441. new("@TenantId", tenantId),
  442. new("@WorkOrd", workOrd)
  443. });
  444. // 3) 同步创建 mes_morder(齐套检查/下达流程依赖)
  445. await _db.Ado.ExecuteCommandAsync(
  446. """
  447. INSERT INTO mes_morder (
  448. Id, morder_no, morder_type, morder_date, morder_state,
  449. product_code, product_name, need_number, morder_production_number,
  450. MaterialSituation, urgent, create_by_name, create_time, update_by_name, update_time,
  451. tenant_id, factory_id, company_id, IsDeleted
  452. ) VALUES (
  453. @Id, @MorderNo, '生产工单', @Now, '新建',
  454. @ProductCode, @ProductName, 1, 1,
  455. '待检查', 0, @User, @Now, @User, @Now,
  456. @TenantId, @FactoryId, @FactoryId, 0
  457. )
  458. """,
  459. new SugarParameter("@Id", YitIdHelper.NextId()),
  460. new SugarParameter("@MorderNo", workOrd),
  461. new SugarParameter("@Now", now),
  462. new SugarParameter("@ProductCode", parentItemNum),
  463. new SugarParameter("@ProductName", string.IsNullOrWhiteSpace(parentDescr) ? string.Empty : parentDescr.Trim()),
  464. new SugarParameter("@User", account),
  465. new SugarParameter("@TenantId", tenantId),
  466. new SugarParameter("@FactoryId", factoryRefId));
  467. return (new WoInfo { RecId = recId, WorkOrd = workOrd, Domain = domain, Status = "p" }, orderCreated);
  468. }
  469. /// <summary>生成 M5 流水工单号(与 OrderWorkOrderGenerationService 同规则)。</summary>
  470. private async Task<string> GenerateWorkOrdNoAsync(long tenantId)
  471. {
  472. const string sql = """
  473. SELECT IFNULL(MAX(CAST(SUBSTRING(TRIM(WorkOrd), 3) AS UNSIGNED)), 0)
  474. FROM WorkOrdMaster
  475. WHERE tenant_id = @TenantId
  476. AND TRIM(WorkOrd) REGEXP '^M5[0-9]+$'
  477. """;
  478. var maxSeq = await _db.Ado.GetIntAsync(sql, new SugarParameter("@TenantId", tenantId));
  479. for (var attempt = 0; attempt < 5; attempt++)
  480. {
  481. var next = maxSeq + 1 + attempt;
  482. var candidate = "M5" + next.ToString("D8", CultureInfo.InvariantCulture);
  483. var dup = await _db.Ado.GetIntAsync(
  484. "SELECT COUNT(*) FROM WorkOrdMaster WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd",
  485. new SugarParameter("@TenantId", tenantId),
  486. new SugarParameter("@WorkOrd", candidate));
  487. if (dup == 0)
  488. return candidate;
  489. }
  490. throw Oops.Oh("生成工单号失败:流水号冲突,请重试");
  491. }
  492. /// <summary>解析 Domain:取本租户现有工单中出现次数最多的 Domain,缺省 8010。</summary>
  493. private async Task<string> ResolveDomainAsync(long tenantId)
  494. {
  495. var fallback = await _db.Ado.GetStringAsync(
  496. """
  497. SELECT `Domain` FROM WorkOrdMaster
  498. WHERE tenant_id = @TenantId
  499. AND `Domain` IS NOT NULL AND TRIM(`Domain`) <> ''
  500. GROUP BY `Domain`
  501. ORDER BY COUNT(*) DESC
  502. LIMIT 1
  503. """,
  504. new SugarParameter("@TenantId", tenantId));
  505. if (!string.IsNullOrWhiteSpace(fallback))
  506. return fallback.Trim();
  507. return "8010";
  508. }
  509. private sealed class WoInfo
  510. {
  511. public long RecId { get; set; }
  512. public string WorkOrd { get; set; } = string.Empty;
  513. public string? Domain { get; set; }
  514. public string? Status { get; set; }
  515. }
  516. private sealed class ExistingDetailRow
  517. {
  518. public long RecID { get; set; }
  519. public string ItemNum { get; set; } = string.Empty;
  520. public decimal QtyRequired { get; set; }
  521. }
  522. }