LinkagePlanService.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. using Admin.NET.Plugin.AiDOP.DataPlatform;
  2. using Admin.NET.Plugin.AiDOP.DataPlatform.S1Refresh;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. namespace Admin.NET.Plugin.AiDOP.Order;
  5. /// <summary>
  6. /// 计划联动看板服务
  7. /// 路由前缀:/api/Order/linkageplan/...
  8. ///
  9. /// 【租户安全边界】需认证访问(已移除类级 <c>[AllowAnonymous]</c>);租户一律经
  10. /// <c>AidopTenantScope.ResolveOrThrow</c> 从认证后 JWT 解析,无有效租户即拒绝,
  11. /// 不读前端 tenantId、无默认回退(原实现:类级匿名 + 直接取 <c>_userManager.TenantId</c>,
  12. /// 匿名时 TenantId=0 与 NULL 比较失败导致查不到数据)。
  13. /// </summary>
  14. [ApiDescriptionSettings(Order = 250, Description = "计划联动看板")]
  15. [Route("api/Order")]
  16. [NonUnify]
  17. public class LinkagePlanService : IDynamicApiController, ITransient
  18. {
  19. private const string SqlServerSourceCode = "DOPDEMORQ_SQLSERVER";
  20. private readonly ISqlSugarClient _db;
  21. private readonly SqlSugarRepository<LinkagePlan> _linkagePlanRep;
  22. private readonly UserManager _userManager;
  23. private readonly S1MdpSyncTransformService _s1MdpSyncTransformService;
  24. private readonly MdpSourceScopeFactory _scopeFactory;
  25. public LinkagePlanService(
  26. ISqlSugarClient db,
  27. SqlSugarRepository<LinkagePlan> linkagePlanRep,
  28. UserManager userManager,
  29. S1MdpSyncTransformService s1MdpSyncTransformService,
  30. MdpSourceScopeFactory scopeFactory)
  31. {
  32. _db = db;
  33. _linkagePlanRep = linkagePlanRep;
  34. _userManager = userManager;
  35. _s1MdpSyncTransformService = s1MdpSyncTransformService;
  36. _scopeFactory = scopeFactory;
  37. }
  38. [DisplayName("获取计划联动看板列表")]
  39. [HttpGet("linkageplan/list")]
  40. public async Task<object> GetLinkagePlanList([FromQuery] LinkagePlanListInput input)
  41. {
  42. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  43. var q = _db.Queryable<LinkagePlan>()
  44. .Where(u => u.TenantId == tenantId)
  45. .WhereIF(!string.IsNullOrWhiteSpace(input.BillNo), u => u.BillNo != null && u.BillNo.Contains(input.BillNo!.Trim()))
  46. .WhereIF(!string.IsNullOrWhiteSpace(input.CustomNo), u => u.CustomNo != null && u.CustomNo.Contains(input.CustomNo!.Trim()))
  47. .WhereIF(!string.IsNullOrWhiteSpace(input.OrderType), u => u.OrderType == input.OrderType!.Trim())
  48. .WhereIF(!string.IsNullOrWhiteSpace(input.ItemNumber), u => u.ItemNumber == input.ItemNumber!.Trim());
  49. var paged = await q
  50. .OrderBy(u => u.BillNo)
  51. .OrderBy(u => u.ItemNumber)
  52. .OrderBy(u => u.LevelNum)
  53. .OrderBy("CASE WHEN type='计划' THEN 1 WHEN type='实际' THEN 2 WHEN type='偏差' THEN 3 ELSE 4 END")
  54. .ToPagedListAsync(input.Page, input.PageSize);
  55. var list = paged.Items.Select(u => new
  56. {
  57. id = u.Id,
  58. billNo = u.BillNo,
  59. customNo = u.CustomNo,
  60. orderType = u.OrderType,
  61. itemNumber = u.ItemNumber,
  62. descr = u.Descr,
  63. descr1 = u.Descr1,
  64. itemType = u.ItemType,
  65. qty = u.Qty,
  66. updateTime = u.UpdateTime?.ToString("yyyy-MM-dd"),
  67. sysCapacityDate = u.SysCapacityDate?.ToString("yyyy-MM-dd"),
  68. type = u.Type,
  69. bomStart = u.BomStart,
  70. bomEnd = u.BomEnd,
  71. lineStart = u.LineStart,
  72. lineEnd = u.LineEnd,
  73. productStart = u.ProductStart,
  74. productEnd = u.ProductEnd,
  75. needTime = u.NeedTime,
  76. sysMaterialDate = u.SysMaterialDate,
  77. cgNeedTime = u.CgNeedTime,
  78. cgEnd = u.CgEnd,
  79. blStart = u.BlStart,
  80. blEnd = u.BlEnd,
  81. startTime = u.StartTime,
  82. endTime = u.EndTime,
  83. ipqcJyStart = u.IpqcJyStart,
  84. ipqcJyEnd = u.IpqcJyEnd,
  85. fqcJyStart = u.FqcJyStart,
  86. fqcJyEnd = u.FqcJyEnd,
  87. rkStart = u.RkStart,
  88. rkEnd = u.RkEnd,
  89. fyStartTime = u.FyStartTime,
  90. fyEndTime = u.FyEndTime,
  91. levelNum = u.LevelNum,
  92. background = u.Background,
  93. businessId = u.BusinessId,
  94. }).ToList();
  95. return new { total = paged.Total, page = input.Page, pageSize = input.PageSize, list };
  96. }
  97. [DisplayName("获取计划联动看板详情")]
  98. [HttpGet("linkageplan/{id:long}")]
  99. public async Task<object> GetLinkagePlanDetail(long id)
  100. {
  101. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  102. var m = await _linkagePlanRep.GetFirstAsync(u => u.Id == id && u.TenantId == tenantId)
  103. ?? throw Oops.Oh("计划联动记录不存在");
  104. return new
  105. {
  106. id = m.Id,
  107. billNo = m.BillNo,
  108. customNo = m.CustomNo,
  109. orderType = m.OrderType,
  110. itemNumber = m.ItemNumber,
  111. descr = m.Descr,
  112. descr1 = m.Descr1,
  113. itemType = m.ItemType,
  114. qty = m.Qty,
  115. updateTime = m.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
  116. sysCapacityDate = m.SysCapacityDate?.ToString("yyyy-MM-dd HH:mm:ss"),
  117. type = m.Type,
  118. bomStart = m.BomStart,
  119. bomEnd = m.BomEnd,
  120. lineStart = m.LineStart,
  121. lineEnd = m.LineEnd,
  122. productStart = m.ProductStart,
  123. productEnd = m.ProductEnd,
  124. needTime = m.NeedTime,
  125. sysMaterialDate = m.SysMaterialDate,
  126. cgNeedTime = m.CgNeedTime,
  127. cgEnd = m.CgEnd,
  128. blStart = m.BlStart,
  129. blEnd = m.BlEnd,
  130. startTime = m.StartTime,
  131. endTime = m.EndTime,
  132. ipqcJyStart = m.IpqcJyStart,
  133. ipqcJyEnd = m.IpqcJyEnd,
  134. fqcJyStart = m.FqcJyStart,
  135. fqcJyEnd = m.FqcJyEnd,
  136. rkStart = m.RkStart,
  137. rkEnd = m.RkEnd,
  138. fyStartTime = m.FyStartTime,
  139. fyEndTime = m.FyEndTime,
  140. levelNum = m.LevelNum,
  141. background = m.Background,
  142. };
  143. }
  144. [DisplayName("刷新计划联动数据")]
  145. [HttpPost("linkageplan/refresh")]
  146. public async Task<object> RefreshLinkagePlanData()
  147. {
  148. try
  149. {
  150. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  151. var result = await _s1MdpSyncTransformService.RunFullAsync(S1MdpRunScope.Create(tenantId, 1), triggerType: "MANUAL");
  152. return new
  153. {
  154. message = "数据刷新成功",
  155. batchId = result.BatchId,
  156. result.StageRows,
  157. result.StandardRows,
  158. result.DwdRows
  159. };
  160. }
  161. catch (Exception ex)
  162. {
  163. throw Oops.Oh($"刷新数据失败: {ex.Message}");
  164. }
  165. }
  166. [DisplayName("执行计划联动刷新")]
  167. [HttpPost("linkageplan/run")]
  168. public async Task<object> RunLinkagePlanProcedure()
  169. {
  170. try
  171. {
  172. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  173. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  174. // ── Step 0: 从 SQL Server 读取 InvTransHist / NbrMaster / NbrDetail,写入 MySQL 辅助表 ──
  175. await PrepareInvTransHistTempAsync();
  176. await PrepareNbrMasterTempAsync();
  177. await PrepareNbrDetailTempAsync();
  178. // ── Step 1: 只清空本租户数据(重算前清理;全表 DELETE 会误删其它租户看板数据)──
  179. await _db.Ado.ExecuteCommandAsync("DELETE FROM LinkagePlan WHERE tenant_id = @TenantId", new SugarParameter("@TenantId", tenantId));
  180. // ── Step 2: 插入计划 + 实际数据(IGNORE 跳过重复主键) ──
  181. await _db.Ado.ExecuteCommandAsync("""
  182. INSERT IGNORE INTO LinkagePlan(
  183. id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
  184. qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
  185. productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
  186. blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
  187. fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, tenant_id
  188. )
  189. SELECT
  190. id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
  191. qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
  192. productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
  193. blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
  194. fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, @TenantId
  195. FROM (
  196. -- 计划数据
  197. SELECT
  198. b.id, a.bill_no, a.custom_no,
  199. IF(a.order_type='1', '销售', '计划') AS order_type,
  200. b.item_number, c.Descr, c.Descr1, c.ownerapplication AS ItemType,
  201. b.qty, h.create_time AS update_time, b.sys_capacity_date,
  202. h.create_time AS bomstart,
  203. DATE_ADD(h.create_time, INTERVAL IFNULL(c.BOMDesignTime, 0) DAY) AS bomend,
  204. DATE_ADD(h.create_time, INTERVAL IFNULL(c.BOMDesignTime, 0) DAY) AS linestart,
  205. DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0)) DAY) AS lineend,
  206. DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0)) DAY) AS productstart,
  207. DATE_ADD(h.create_time, INTERVAL (IFNULL(c.BOMDesignTime, 0) + IFNULL(c.RoutingDesignTime, 0) +
  208. CEILING(IFNULL(((b.qty-1)*d.maxruntime+d.runtime)/8, 0))) DAY) AS productend,
  209. h.create_time AS needtime, b.sys_material_date,
  210. h.create_time AS cgneedtime, e.kitting_time AS cgend,
  211. e.kitting_time AS blstart,
  212. DATE_ADD(e.kitting_time, INTERVAL IFNULL(c.mfgmttr, 0) DAY) AS blend,
  213. g.starttime, g.endtime,
  214. g.endtime AS ipqcjystart,
  215. DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS ipqcjyend,
  216. g.endtime AS fqcjystart,
  217. DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS fqcjyend,
  218. DATE_ADD(g.endtime, INTERVAL IFNULL(c.InsLT, 0) DAY) AS rkstart,
  219. DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 1) DAY) AS rkend,
  220. DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 1) DAY) AS fystarttime,
  221. DATE_ADD(g.endtime, INTERVAL (IFNULL(c.InsLT, 0) + 2) DAY) AS fyendtime,
  222. '计划' AS `type`, 2 AS levelnum, 0 AS isuse
  223. FROM crm_seorder a
  224. LEFT JOIN crm_seorderentry b ON a.bill_no = b.bill_no AND b.tenant_id = a.tenant_id
  225. LEFT JOIN ItemMaster c ON b.item_number = c.ItemNum AND (c.tenant_id = a.tenant_id OR a.tenant_id IS NULL)
  226. LEFT JOIN (
  227. SELECT RoutingCode, SUM(runtime) AS runtime, MAX(runtime) AS maxruntime
  228. FROM RoutingOpDetail GROUP BY RoutingCode
  229. ) d ON c.ItemNum = d.RoutingCode
  230. LEFT JOIN (
  231. SELECT sentry_id, MAX(kitting_time) AS kitting_time
  232. FROM (
  233. SELECT ar.sentry_id, bce.kitting_time,
  234. ROW_NUMBER() OVER (PARTITION BY ar.sentry_id ORDER BY ar.create_time DESC) AS rn
  235. FROM b_examine_result ar
  236. JOIN b_bom_child_examine bce ON ar.id = bce.examine_id
  237. WHERE ar.sorderid IS NOT NULL AND bce.erp_cls IN ('2','3')
  238. AND ar.tenant_id = @TenantId
  239. ) ex WHERE rn = 1
  240. GROUP BY sentry_id
  241. ) e ON b.id = e.sentry_id
  242. LEFT JOIN mes_moentry f ON b.id = f.soentry_id AND f.tenant_id = a.tenant_id
  243. LEFT JOIN mes_morder h ON f.moentry_mono = h.morder_no AND h.parent_id IS NULL AND h.tenant_id = a.tenant_id
  244. LEFT JOIN (
  245. SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
  246. FROM PeriodSequenceDet GROUP BY workords
  247. ) g ON f.moentry_mono = g.workords
  248. WHERE a.IsDeleted = 0 AND h.create_time IS NOT NULL
  249. AND a.tenant_id = @TenantId
  250. UNION ALL
  251. -- 实际数据(InvTransHist 从 SQL Server 临时表读取)
  252. SELECT
  253. b.id, a.bill_no, a.custom_no,
  254. IF(a.order_type='1', '销售', '计划') AS order_type,
  255. b.item_number, c.Descr, c.Descr1, c.ownerapplication AS ItemType,
  256. b.qty, h.create_time AS update_time, b.sys_capacity_date,
  257. h.create_time AS bomstart, nb.CreateTime AS bomend,
  258. h.create_time AS linestart, nb.CreateTime AS lineend,
  259. d.starttime AS productstart, d.endtime AS productend,
  260. h.create_time AS needtime, b.sys_material_date,
  261. e.cgneedtime, e.kitting_time AS cgend,
  262. nb.CreateTime AS blstart, nd.blend,
  263. g.starttime, g.endtime,
  264. e.FAPPLYTIME AS ipqcjystart, e.FINSPESTARTDATE AS ipqcjyend,
  265. g.endtime AS fqcjystart, fqc.fqcDate AS fqcjyend,
  266. i.starttime AS rkstart, i.endtime AS rkend,
  267. asn.starttime AS fystarttime, asn.endtime AS fyendtime,
  268. '实际' AS `type`, 1 AS levelnum, 0 AS isuse
  269. FROM crm_seorder a
  270. LEFT JOIN crm_seorderentry b ON a.bill_no = b.bill_no AND b.tenant_id = a.tenant_id
  271. LEFT JOIN ItemMaster c ON b.item_number = c.ItemNum AND (c.tenant_id = a.tenant_id OR a.tenant_id IS NULL)
  272. LEFT JOIN (
  273. SELECT pr.sentry_id,
  274. MAX(sh.createtime) AS kitting_time,
  275. MIN(jq.jqhf) AS cgneedtime,
  276. MIN(iqcjy.FINSPESTARTDATE) AS FINSPESTARTDATE,
  277. MAX(iqcjy.FAPPLYTIME) AS FAPPLYTIME
  278. FROM srm_pr_main pr
  279. LEFT JOIN PurOrdDetail pod ON pr.pr_billno = pod.Req AND pod.tenant_id = pr.tenant_id
  280. LEFT JOIN vscm_cgshrk sh ON pod.PurOrd = sh.OrdNbr AND pod.Line = sh.OrdLine
  281. LEFT JOIN scm_jhjh_jq jq ON pod.PurOrd = jq.cgdd AND pod.Line = jq.ddhh
  282. LEFT JOIN (
  283. SELECT f.po_bill, f.po_billline,
  284. MIN(a2.FINSPESTARTDATE) AS FINSPESTARTDATE,
  285. MAX(b2.FAPPLYTIME) AS FAPPLYTIME
  286. FROM qms_qcp_inspbill a2
  287. LEFT JOIN qms_qcp_inspecapplyn b2 ON a2.lydjbh = b2.FBILLNO
  288. LEFT JOIN qms_qcp_insappnentry c2 ON b2.id = c2.glid
  289. JOIN scm_shd cd ON c2.shdh = cd.shddh
  290. LEFT JOIN scm_shdzb f ON f.shpc = c2.FLOTNUMBER
  291. AND f.sh_material_code = c2.FMATERIALCFG
  292. AND f.glid = cd.id
  293. GROUP BY f.po_bill, f.po_billline
  294. ) iqcjy ON pod.PurOrd = iqcjy.po_bill AND pod.Line = iqcjy.po_billline
  295. WHERE jq.flag = '1' AND pr.sentry_id IS NOT NULL
  296. AND pr.tenant_id = @TenantId
  297. GROUP BY pr.sentry_id
  298. ) e ON e.sentry_id = b.id
  299. LEFT JOIN mes_moentry f ON b.id = f.soentry_id AND f.tenant_id = a.tenant_id
  300. LEFT JOIN mes_morder h ON f.moentry_mono = h.morder_no AND h.parent_id IS NULL AND h.tenant_id = a.tenant_id
  301. LEFT JOIN (
  302. SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
  303. FROM PeriodSequenceDet GROUP BY workords
  304. ) d ON h.morder_no = d.workords
  305. LEFT JOIN _tmp_nbr_master nb ON h.morder_no = nb.WorkOrd
  306. LEFT JOIN _tmp_nbr_detail nd ON h.morder_no = nd.WorkOrd
  307. LEFT JOIN (
  308. SELECT workords, MIN(plandate) AS starttime, MAX(plandate) AS endtime
  309. FROM PeriodSequenceDet WHERE CompQty > 0 GROUP BY workords
  310. ) g ON f.moentry_mono = g.workords
  311. LEFT JOIN _tmp_inv_trans_hist i ON h.morder_no = i.WorkOrd
  312. LEFT JOIN (
  313. SELECT ad.OrdNbr, ad.ContainerItem, ad.OrdLine,
  314. MIN(ad.CreateTime) AS starttime, MAX(ad.UpdateTime) AS endtime
  315. FROM ASNBOLShipperDetail ad
  316. INNER JOIN ASNBOLShipperMaster am ON ad.ASNBOLShipperRecID = am.RecID
  317. WHERE am.shtype = 'SH' AND am.typed <> 'S'
  318. GROUP BY ad.OrdNbr, ad.ContainerItem, ad.OrdLine
  319. ) asn ON b.bill_no = asn.OrdNbr AND b.item_number = asn.ContainerItem
  320. LEFT JOIN WorkOrdMaster wm ON h.morder_no = wm.WorkOrd AND wm.tenant_id = a.tenant_id
  321. LEFT JOIN (
  322. SELECT sczld, MAX(FINSPEENDDATE) AS fqcDate
  323. FROM qms_qcpp_inspbill GROUP BY sczld
  324. ) fqc ON wm.Batch = fqc.sczld
  325. WHERE a.IsDeleted = 0 AND h.create_time IS NOT NULL
  326. AND a.tenant_id = @TenantId
  327. ) AS viewall
  328. """, pars);
  329. // ── Step 3: 插入偏差数据 ──
  330. await _db.Ado.ExecuteCommandAsync("""
  331. INSERT IGNORE INTO LinkagePlan(
  332. id, bill_no, custom_no, order_type, item_number, Descr, Descr1, ItemType,
  333. qty, update_time, sys_capacity_date, bomstart, bomend, linestart, lineend,
  334. productstart, productend, needtime, sys_material_date, cgneedtime, cgend,
  335. blstart, blend, starttime, endtime, ipqcjystart, ipqcjyend, fqcjystart,
  336. fqcjyend, rkstart, rkend, fystarttime, fyendtime, `type`, levelnum, isuse, tenant_id
  337. )
  338. SELECT
  339. a.id, a.bill_no, a.custom_no, a.order_type, b.item_number,
  340. a.Descr, a.Descr1, a.ItemType, b.qty, a.update_time, b.sys_capacity_date,
  341. DATEDIFF(b.bomstart, a.bomstart), DATEDIFF(b.bomend, a.bomend),
  342. DATEDIFF(b.linestart, a.linestart), DATEDIFF(b.lineend, a.lineend),
  343. DATEDIFF(b.productstart, a.productstart), DATEDIFF(b.productend, a.productend),
  344. DATEDIFF(b.needtime, a.needtime), DATEDIFF(b.sys_material_date, a.sys_material_date),
  345. DATEDIFF(b.cgneedtime, a.cgneedtime), DATEDIFF(b.cgend, a.cgend),
  346. DATEDIFF(b.blstart, a.blstart), DATEDIFF(b.blend, a.blend),
  347. DATEDIFF(b.starttime, a.starttime), DATEDIFF(b.endtime, a.endtime),
  348. DATEDIFF(b.ipqcjystart, a.ipqcjystart), DATEDIFF(b.ipqcjyend, a.ipqcjyend),
  349. DATEDIFF(b.fqcjystart, a.fqcjystart), DATEDIFF(b.fqcjyend, a.fqcjyend),
  350. DATEDIFF(b.rkstart, a.rkstart), DATEDIFF(b.rkend, a.rkend),
  351. DATEDIFF(b.fystarttime, a.fystarttime), DATEDIFF(b.fyendtime, a.fyendtime),
  352. '偏差', 0, 0, @TenantId
  353. FROM (SELECT * FROM LinkagePlan WHERE `type` = '计划' AND tenant_id = @TenantId) a
  354. LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '实际' AND tenant_id = @TenantId) b ON a.id = b.id
  355. """, pars);
  356. // ── Step 4: 更新背景色标记 ──
  357. await _db.Ado.ExecuteCommandAsync("""
  358. UPDATE LinkagePlan a
  359. LEFT JOIN (
  360. SELECT a2.id, a2.`type`,
  361. CONCAT(
  362. CASE WHEN a2.`type` = '计划' THEN
  363. CASE WHEN a2.bomstart IS NOT NULL AND b2.bomstart IS NULL AND DATEDIFF(NOW(), a2.bomstart) > 7 THEN 'bomstart:red;'
  364. WHEN a2.bomstart IS NOT NULL AND b2.bomstart IS NULL AND DATEDIFF(NOW(), a2.bomstart) > 0 THEN 'bomstart:yellow;'
  365. ELSE '' END
  366. WHEN a2.`type` = '偏差' THEN
  367. CASE WHEN IFNULL(CAST(a2.bomstart AS SIGNED), 0) > 7 THEN 'bomstart:red;'
  368. WHEN IFNULL(CAST(a2.bomstart AS SIGNED), 0) > 0 THEN 'bomstart:yellow;'
  369. ELSE '' END
  370. ELSE '' END,
  371. CASE WHEN a2.`type` = '计划' THEN
  372. CASE WHEN a2.bomend IS NOT NULL AND b2.bomend IS NULL AND DATEDIFF(NOW(), a2.bomend) > 7 THEN 'bomend:red;'
  373. WHEN a2.bomend IS NOT NULL AND b2.bomend IS NULL AND DATEDIFF(NOW(), a2.bomend) > 0 THEN 'bomend:yellow;'
  374. ELSE '' END
  375. WHEN a2.`type` = '偏差' THEN
  376. CASE WHEN IFNULL(CAST(a2.bomend AS SIGNED), 0) > 7 THEN 'bomend:red;'
  377. WHEN IFNULL(CAST(a2.bomend AS SIGNED), 0) > 0 THEN 'bomend:yellow;'
  378. ELSE '' END
  379. ELSE '' END,
  380. CASE WHEN a2.`type` = '计划' THEN
  381. CASE WHEN a2.starttime IS NOT NULL AND b2.starttime IS NULL AND DATEDIFF(NOW(), a2.starttime) > 7 THEN 'starttime:red;'
  382. WHEN a2.starttime IS NOT NULL AND b2.starttime IS NULL AND DATEDIFF(NOW(), a2.starttime) > 0 THEN 'starttime:yellow;'
  383. ELSE '' END
  384. WHEN a2.`type` = '偏差' THEN
  385. CASE WHEN IFNULL(CAST(a2.starttime AS SIGNED), 0) > 7 THEN 'starttime:red;'
  386. WHEN IFNULL(CAST(a2.starttime AS SIGNED), 0) > 0 THEN 'starttime:yellow;'
  387. ELSE '' END
  388. ELSE '' END,
  389. CASE WHEN a2.`type` = '计划' THEN
  390. CASE WHEN a2.endtime IS NOT NULL AND b2.endtime IS NULL AND DATEDIFF(NOW(), a2.endtime) > 7 THEN 'endtime:red;'
  391. WHEN a2.endtime IS NOT NULL AND b2.endtime IS NULL AND DATEDIFF(NOW(), a2.endtime) > 0 THEN 'endtime:yellow;'
  392. ELSE '' END
  393. WHEN a2.`type` = '偏差' THEN
  394. CASE WHEN IFNULL(CAST(a2.endtime AS SIGNED), 0) > 7 THEN 'endtime:red;'
  395. WHEN IFNULL(CAST(a2.endtime AS SIGNED), 0) > 0 THEN 'endtime:yellow;'
  396. ELSE '' END
  397. ELSE '' END
  398. ) AS background
  399. FROM LinkagePlan a2
  400. LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '实际' AND tenant_id = @TenantId) b2 ON a2.id = b2.id
  401. LEFT JOIN (SELECT * FROM LinkagePlan WHERE `type` = '偏差' AND tenant_id = @TenantId) c2 ON a2.id = c2.id
  402. WHERE a2.tenant_id = @TenantId
  403. ) AS bg ON a.id = bg.id AND a.`type` = bg.`type`
  404. SET a.background = bg.background
  405. WHERE a.tenant_id = @TenantId
  406. """, pars);
  407. // ── 清理辅助表 ──
  408. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_inv_trans_hist");
  409. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_master");
  410. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_detail");
  411. return new { message = "计划联动刷新成功" };
  412. }
  413. catch (Exception ex)
  414. {
  415. throw Oops.Oh($"执行计划联动刷新失败: {ex.Message}");
  416. }
  417. }
  418. /// <summary>
  419. /// 从 SQL Server 读取 InvTransHist,写入 MySQL 临时表 _tmp_inv_trans_hist。
  420. /// </summary>
  421. private async Task PrepareInvTransHistTempAsync()
  422. {
  423. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_inv_trans_hist");
  424. await _db.Ado.ExecuteCommandAsync("""
  425. CREATE TABLE IF NOT EXISTS _tmp_inv_trans_hist (
  426. WorkOrd VARCHAR(64) PRIMARY KEY,
  427. starttime DATETIME NULL,
  428. endtime DATETIME NULL
  429. )
  430. """);
  431. var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
  432. var rows = await remote.Ado.SqlQueryAsync<InvTransHistRow>("""
  433. SELECT WorkOrd,
  434. MIN(CreateTime) AS starttime,
  435. MAX(CreateTime) AS endtime
  436. FROM InvTransHist
  437. WHERE TransType = 'rct-wo' AND QtyChange > 0
  438. GROUP BY WorkOrd
  439. """);
  440. if (rows.Count == 0)
  441. return;
  442. // 分批 INSERT(每批 500 条)
  443. var insertPars = new List<SugarParameter>();
  444. for (var i = 0; i < rows.Count; i++)
  445. {
  446. var r = rows[i];
  447. if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
  448. insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
  449. insertPars.Add(new SugarParameter($"@st{i}", r.StartTime));
  450. insertPars.Add(new SugarParameter($"@et{i}", r.EndTime));
  451. }
  452. const int batchSize = 500;
  453. for (var offset = 0; offset < insertPars.Count; offset += batchSize * 3)
  454. {
  455. var batchEnd = Math.Min(offset + batchSize * 3, insertPars.Count);
  456. var batchPars = insertPars.GetRange(offset, batchEnd - offset);
  457. var batchValues = new List<string>();
  458. for (var j = offset; j < batchEnd; j += 3)
  459. batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName}, {insertPars[j+2].ParameterName})");
  460. await _db.Ado.ExecuteCommandAsync(
  461. $"INSERT INTO _tmp_inv_trans_hist (WorkOrd, starttime, endtime) VALUES {string.Join(",", batchValues)}",
  462. batchPars);
  463. }
  464. }
  465. private sealed class InvTransHistRow
  466. {
  467. public string? WorkOrd { get; set; }
  468. public DateTime? StartTime { get; set; }
  469. public DateTime? EndTime { get; set; }
  470. }
  471. /// <summary>
  472. /// 从 SQL Server 读取 NbrMaster (Type='SM'),写入 MySQL 临时表 _tmp_nbr_master。
  473. /// </summary>
  474. private async Task PrepareNbrMasterTempAsync()
  475. {
  476. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_master");
  477. await _db.Ado.ExecuteCommandAsync("""
  478. CREATE TABLE IF NOT EXISTS _tmp_nbr_master (
  479. WorkOrd VARCHAR(64) PRIMARY KEY,
  480. CreateTime DATETIME NULL
  481. )
  482. """);
  483. var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
  484. var rows = await remote.Ado.SqlQueryAsync<NbrMasterRow>("""
  485. SELECT WorkOrd, MIN(CreateTime) AS CreateTime
  486. FROM NbrMaster WHERE Type = 'SM'
  487. GROUP BY WorkOrd
  488. """);
  489. if (rows.Count == 0)
  490. return;
  491. var insertPars = new List<SugarParameter>();
  492. for (var i = 0; i < rows.Count; i++)
  493. {
  494. var r = rows[i];
  495. if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
  496. insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
  497. insertPars.Add(new SugarParameter($"@ct{i}", r.CreateTime));
  498. }
  499. const int batchSize = 500;
  500. for (var offset = 0; offset < insertPars.Count; offset += batchSize * 2)
  501. {
  502. var batchEnd = Math.Min(offset + batchSize * 2, insertPars.Count);
  503. var batchPars = insertPars.GetRange(offset, batchEnd - offset);
  504. var batchValues = new List<string>();
  505. for (var j = offset; j < batchEnd; j += 2)
  506. batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName})");
  507. await _db.Ado.ExecuteCommandAsync(
  508. $"INSERT INTO _tmp_nbr_master (WorkOrd, CreateTime) VALUES {string.Join(",", batchValues)}",
  509. batchPars);
  510. }
  511. }
  512. /// <summary>
  513. /// 从 SQL Server 读取 NbrDetail,写入 MySQL 临时表 _tmp_nbr_detail。
  514. /// </summary>
  515. private async Task PrepareNbrDetailTempAsync()
  516. {
  517. await _db.Ado.ExecuteCommandAsync("DROP TABLE IF EXISTS _tmp_nbr_detail");
  518. await _db.Ado.ExecuteCommandAsync("""
  519. CREATE TABLE IF NOT EXISTS _tmp_nbr_detail (
  520. WorkOrd VARCHAR(64) PRIMARY KEY,
  521. blend DATETIME NULL
  522. )
  523. """);
  524. var remote = await _scopeFactory.GetScopeAsync(SqlServerSourceCode);
  525. var rows = await remote.Ado.SqlQueryAsync<NbrDetailRow>("""
  526. SELECT WorkOrd, MIN(UpdateTime) AS blend
  527. FROM NbrDetail
  528. GROUP BY WorkOrd
  529. """);
  530. if (rows.Count == 0)
  531. return;
  532. var insertPars = new List<SugarParameter>();
  533. for (var i = 0; i < rows.Count; i++)
  534. {
  535. var r = rows[i];
  536. if (string.IsNullOrWhiteSpace(r.WorkOrd)) continue;
  537. insertPars.Add(new SugarParameter($"@wo{i}", r.WorkOrd));
  538. insertPars.Add(new SugarParameter($"@bl{i}", r.Blend));
  539. }
  540. const int batchSize = 500;
  541. for (var offset = 0; offset < insertPars.Count; offset += batchSize * 2)
  542. {
  543. var batchEnd = Math.Min(offset + batchSize * 2, insertPars.Count);
  544. var batchPars = insertPars.GetRange(offset, batchEnd - offset);
  545. var batchValues = new List<string>();
  546. for (var j = offset; j < batchEnd; j += 2)
  547. batchValues.Add($"({insertPars[j].ParameterName}, {insertPars[j+1].ParameterName})");
  548. await _db.Ado.ExecuteCommandAsync(
  549. $"INSERT INTO _tmp_nbr_detail (WorkOrd, blend) VALUES {string.Join(",", batchValues)}",
  550. batchPars);
  551. }
  552. }
  553. private sealed class NbrMasterRow
  554. {
  555. public string? WorkOrd { get; set; }
  556. public DateTime? CreateTime { get; set; }
  557. }
  558. private sealed class NbrDetailRow
  559. {
  560. public string? WorkOrd { get; set; }
  561. public DateTime? Blend { get; set; }
  562. }
  563. }