ProductionScheduleGenerationService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. namespace Admin.NET.Plugin.AiDOP.Production;
  2. /// <summary>生产排程生成:为待排工单写入 PeriodSequenceDet(工作日历 + 工作中心冲突避让)。</summary>
  3. public class ProductionScheduleGenerationService : ITransient
  4. {
  5. private readonly ISqlSugarClient _db;
  6. public ProductionScheduleGenerationService(ISqlSugarClient db)
  7. {
  8. _db = db;
  9. }
  10. public async Task<ScheduleGenerationResult> GenerateAsync(long tenantId, string? domain, string account)
  11. {
  12. var workOrders = await LoadPendingWorkOrdersAsync(tenantId);
  13. if (workOrders.Count == 0)
  14. return new ScheduleGenerationResult { Message = "没有待排产的工单(状态 p/r)" };
  15. return await ScheduleWorkOrdersAsync(tenantId, domain, account, workOrders);
  16. }
  17. public async Task<ScheduleGenerationResult> RegenerateForWorkOrderAsync(
  18. long tenantId,
  19. string workOrd,
  20. string? domain,
  21. string account)
  22. {
  23. var rows = await _db.Ado.SqlQueryAsync<PendingWorkOrderRow>(
  24. """
  25. SELECT RecID AS RecId, WorkOrd, ItemNum, `Domain`, QtyOrded, OrdDate, DueDate, Priority, Urgent
  26. FROM WorkOrdMaster
  27. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd
  28. LIMIT 1
  29. """,
  30. new SugarParameter("@TenantId", tenantId),
  31. new SugarParameter("@WorkOrd", workOrd.Trim()));
  32. var wo = rows.FirstOrDefault();
  33. if (wo is null)
  34. return new ScheduleGenerationResult { Message = $"工单 {workOrd} 不存在" };
  35. return await ScheduleWorkOrdersAsync(tenantId, domain, account, new List<PendingWorkOrderRow> { wo });
  36. }
  37. private async Task<ScheduleGenerationResult> ScheduleWorkOrdersAsync(
  38. long tenantId,
  39. string? domain,
  40. string account,
  41. List<PendingWorkOrderRow> workOrders)
  42. {
  43. var now = DateTime.Now;
  44. var calendar = await LoadWorkCenterCalendarAsync(tenantId);
  45. var occupiedSlots = await LoadOccupiedSlotsAsync(tenantId);
  46. var usedCalendar = calendar.Count > 0;
  47. var scheduledCount = 0;
  48. var rowCount = 0;
  49. var skipped = new List<string>();
  50. foreach (var wo in workOrders)
  51. {
  52. var routings = await LoadRoutingsAsync(tenantId, wo.WorkOrd);
  53. if (routings.Count == 0)
  54. {
  55. skipped.Add($"{wo.WorkOrd}(无工艺路线)");
  56. continue;
  57. }
  58. var woDomain = ResolveDomain(wo.Domain, domain, tenantId);
  59. await DeactivateExistingScheduleAsync(tenantId, wo.WorkOrd, woDomain);
  60. var planStart = (wo.OrdDate ?? now).Date;
  61. var planEnd = (wo.DueDate ?? planStart.AddDays(Math.Max(routings.Count, 7))).Date;
  62. if (planEnd < planStart)
  63. planEnd = planStart;
  64. var cursor = planStart;
  65. var seq = 1;
  66. for (var i = 0; i < routings.Count; i++)
  67. {
  68. var routing = routings[i];
  69. DateTime planDate;
  70. if (usedCalendar)
  71. {
  72. planDate = ResolveNextAvailablePlanDate(cursor, routing.WorkCtr, calendar, occupiedSlots);
  73. cursor = planDate.AddDays(1);
  74. }
  75. else
  76. {
  77. var spanDays = Math.Max((planEnd - planStart).Days, 0);
  78. planDate = routings.Count <= 1
  79. ? planStart
  80. : planStart.AddDays(spanDays * i / Math.Max(routings.Count - 1, 1));
  81. }
  82. if (planDate > planEnd)
  83. planEnd = planDate;
  84. var slotKey = BuildOccupancyKey(routing.WorkCtr, planDate);
  85. occupiedSlots.Add(slotKey);
  86. var recId = await NextPeriodRecIdAsync();
  87. await InsertScheduleRowAsync(
  88. recId, woDomain, routing, wo, planDate, seq, account, now, tenantId);
  89. seq++;
  90. rowCount++;
  91. }
  92. scheduledCount++;
  93. }
  94. return new ScheduleGenerationResult
  95. {
  96. WorkOrderCount = scheduledCount,
  97. ScheduleRowCount = rowCount,
  98. SkippedWorkOrders = skipped,
  99. UsedWorkCenterCalendar = usedCalendar,
  100. Message = scheduledCount > 0
  101. ? $"已为 {scheduledCount} 个工单生成 {rowCount} 条工序排程"
  102. : "未生成排程,请确认工单已同步工艺路线"
  103. };
  104. }
  105. private async Task InsertScheduleRowAsync(
  106. int recId,
  107. string woDomain,
  108. RoutingRow routing,
  109. PendingWorkOrderRow wo,
  110. DateTime planDate,
  111. int seq,
  112. string account,
  113. DateTime now,
  114. long tenantId)
  115. {
  116. await _db.Ado.ExecuteCommandAsync(
  117. """
  118. INSERT INTO PeriodSequenceDet (
  119. RecID, `Domain`, Site, ItemNum, Line, Op, WorkCtr, ProdDate, PlanDate,
  120. Sequence, OrdQty, CompQty, WorkOrds, Status, Employee,
  121. CreateUser, CreateTime, UpdateUser, UpdateTime,
  122. IsActive, IsConfirm, BusinessID, tenant_id
  123. ) VALUES (
  124. @RecId, @Domain, @Site, @ItemNum, @Line, @Op, @WorkCtr, @ProdDate, @PlanDate,
  125. @Sequence, @OrdQty, 0, @WorkOrd, '', '',
  126. @User, @Now, @User, @Now,
  127. 1, 0, @BusinessId, @TenantId
  128. )
  129. """,
  130. new SugarParameter("@RecId", recId),
  131. new SugarParameter("@Domain", woDomain.Length > 8 ? woDomain[..8] : woDomain),
  132. new SugarParameter("@Site", routing.Site ?? (object)DBNull.Value),
  133. new SugarParameter("@ItemNum", wo.ItemNum ?? string.Empty),
  134. new SugarParameter("@Line", routing.ProdLine ?? (object)DBNull.Value),
  135. new SugarParameter("@Op", routing.Op),
  136. new SugarParameter("@WorkCtr", routing.WorkCtr ?? (object)DBNull.Value),
  137. new SugarParameter("@ProdDate", planDate),
  138. new SugarParameter("@PlanDate", planDate),
  139. new SugarParameter("@Sequence", seq),
  140. new SugarParameter("@OrdQty", wo.QtyOrded ?? 0),
  141. new SugarParameter("@WorkOrd", wo.WorkOrd),
  142. new SugarParameter("@BusinessId", wo.RecId),
  143. new SugarParameter("@User", account.Length > 24 ? account[..24] : account),
  144. new SugarParameter("@Now", now),
  145. new SugarParameter("@TenantId", tenantId));
  146. }
  147. private static DateTime ResolveNextAvailablePlanDate(
  148. DateTime start,
  149. string? workCtr,
  150. Dictionary<string, HashSet<int>> calendar,
  151. HashSet<string> occupiedSlots)
  152. {
  153. var date = start.Date;
  154. for (var guard = 0; guard < 366; guard++)
  155. {
  156. if (IsWorkDay(date, workCtr, calendar))
  157. {
  158. var key = BuildOccupancyKey(workCtr, date);
  159. if (!occupiedSlots.Contains(key))
  160. return date;
  161. }
  162. date = date.AddDays(1);
  163. }
  164. return start.Date;
  165. }
  166. private static bool IsWorkDay(DateTime date, string? workCtr, Dictionary<string, HashSet<int>> calendar)
  167. {
  168. if (calendar.Count == 0)
  169. return true;
  170. var key = NormalizeWorkCtr(workCtr);
  171. if (!calendar.TryGetValue(key, out var days) || days.Count == 0)
  172. return date.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday;
  173. var weekDay = (int)date.DayOfWeek;
  174. return days.Contains(weekDay);
  175. }
  176. private static string BuildOccupancyKey(string? workCtr, DateTime date) =>
  177. $"{NormalizeWorkCtr(workCtr)}|{date:yyyy-MM-dd}";
  178. private static string NormalizeWorkCtr(string? workCtr) =>
  179. string.IsNullOrWhiteSpace(workCtr) ? "*" : workCtr.Trim();
  180. private async Task<Dictionary<string, HashSet<int>>> LoadWorkCenterCalendarAsync(long tenantId)
  181. {
  182. var rows = await _db.Ado.SqlQueryAsync<CalendarRow>(
  183. """
  184. SELECT TRIM(IFNULL(WorkCtr,'')) AS WorkCtr, WeekDay
  185. FROM ShopCalendarWorkCtr
  186. WHERE tenant_id = @TenantId
  187. AND IFNULL(IsActive, 0) = 1
  188. AND IFNULL(IsWorkDay, 0) = 1
  189. AND WeekDay IS NOT NULL
  190. """,
  191. new SugarParameter("@TenantId", tenantId));
  192. var map = new Dictionary<string, HashSet<int>>(StringComparer.OrdinalIgnoreCase);
  193. foreach (var row in rows)
  194. {
  195. if (row.WeekDay is null) continue;
  196. var key = NormalizeWorkCtr(row.WorkCtr);
  197. if (!map.TryGetValue(key, out var set))
  198. {
  199. set = new HashSet<int>();
  200. map[key] = set;
  201. }
  202. set.Add(row.WeekDay.Value);
  203. }
  204. return map;
  205. }
  206. private async Task<HashSet<string>> LoadOccupiedSlotsAsync(long tenantId)
  207. {
  208. var rows = await _db.Ado.SqlQueryAsync<OccupiedSlotRow>(
  209. """
  210. SELECT TRIM(IFNULL(WorkCtr,'')) AS WorkCtr, DATE(PlanDate) AS PlanDate
  211. FROM PeriodSequenceDet
  212. WHERE tenant_id = @TenantId AND IFNULL(IsActive, 0) = 1 AND PlanDate IS NOT NULL
  213. """,
  214. new SugarParameter("@TenantId", tenantId));
  215. return rows
  216. .Where(x => x.PlanDate.HasValue)
  217. .Select(x => BuildOccupancyKey(x.WorkCtr, x.PlanDate!.Value))
  218. .ToHashSet(StringComparer.OrdinalIgnoreCase);
  219. }
  220. private async Task<List<PendingWorkOrderRow>> LoadPendingWorkOrdersAsync(long tenantId)
  221. {
  222. return await _db.Ado.SqlQueryAsync<PendingWorkOrderRow>(
  223. """
  224. SELECT RecID AS RecId, WorkOrd, ItemNum, `Domain`, QtyOrded, OrdDate, DueDate, Priority, Urgent
  225. FROM WorkOrdMaster
  226. WHERE tenant_id = @TenantId
  227. AND LOWER(TRIM(IFNULL(Status,''))) IN ('p', 'r')
  228. ORDER BY IFNULL(Urgent, 0) DESC, IFNULL(Priority, 0) DESC, DueDate, WorkOrd
  229. """,
  230. new SugarParameter("@TenantId", tenantId));
  231. }
  232. private async Task<List<RoutingRow>> LoadRoutingsAsync(long tenantId, string workOrd)
  233. {
  234. return await _db.Ado.SqlQueryAsync<RoutingRow>(
  235. """
  236. SELECT OP AS Op, ProdLine, WorkCtr, WorkCtr AS Site
  237. FROM WorkOrdRouting
  238. WHERE tenant_id = @TenantId AND WorkOrd = @WorkOrd AND IFNULL(IsActive, 0) = 1
  239. ORDER BY (OP + 0), OP
  240. """,
  241. new SugarParameter("@TenantId", tenantId),
  242. new SugarParameter("@WorkOrd", workOrd));
  243. }
  244. public async Task DeactivateExistingScheduleAsync(long tenantId, string workOrd, string domain)
  245. {
  246. await _db.Ado.ExecuteCommandAsync(
  247. """
  248. UPDATE PeriodSequenceDet
  249. SET IsActive = 0, UpdateTime = @Now
  250. WHERE tenant_id = @TenantId AND WorkOrds = @WorkOrd AND `Domain` = @Domain AND IFNULL(IsActive, 0) = 1
  251. """,
  252. new SugarParameter("@Now", DateTime.Now),
  253. new SugarParameter("@TenantId", tenantId),
  254. new SugarParameter("@WorkOrd", workOrd),
  255. new SugarParameter("@Domain", domain.Length > 8 ? domain[..8] : domain));
  256. }
  257. private static string ResolveDomain(string? woDomain, string? requestDomain, long tenantId)
  258. {
  259. if (!string.IsNullOrWhiteSpace(woDomain))
  260. return woDomain.Trim();
  261. if (!string.IsNullOrWhiteSpace(requestDomain))
  262. return requestDomain.Trim();
  263. return tenantId.ToString();
  264. }
  265. private async Task<int> NextPeriodRecIdAsync()
  266. {
  267. var max = await _db.Ado.GetIntAsync("SELECT IFNULL(MAX(RecID), 0) FROM PeriodSequenceDet");
  268. return max + 1;
  269. }
  270. public sealed class ScheduleGenerationResult
  271. {
  272. public int WorkOrderCount { get; set; }
  273. public int ScheduleRowCount { get; set; }
  274. public bool UsedWorkCenterCalendar { get; set; }
  275. public List<string> SkippedWorkOrders { get; set; } = new();
  276. public string Message { get; set; } = string.Empty;
  277. }
  278. private sealed class PendingWorkOrderRow
  279. {
  280. public long RecId { get; set; }
  281. public string WorkOrd { get; set; } = string.Empty;
  282. public string? ItemNum { get; set; }
  283. public string? Domain { get; set; }
  284. public decimal? QtyOrded { get; set; }
  285. public DateTime? OrdDate { get; set; }
  286. public DateTime? DueDate { get; set; }
  287. public decimal? Priority { get; set; }
  288. public int? Urgent { get; set; }
  289. }
  290. private sealed class RoutingRow
  291. {
  292. public int Op { get; set; }
  293. public string? ProdLine { get; set; }
  294. public string? WorkCtr { get; set; }
  295. public string? Site { get; set; }
  296. }
  297. private sealed class CalendarRow
  298. {
  299. public string? WorkCtr { get; set; }
  300. public int? WeekDay { get; set; }
  301. }
  302. private sealed class OccupiedSlotRow
  303. {
  304. public string? WorkCtr { get; set; }
  305. public DateTime? PlanDate { get; set; }
  306. }
  307. }