ShopCalendarWorkCtrService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. namespace Admin.NET.Plugin.AiDOP.Production;
  2. /// <summary>
  3. /// 产线工作日历(ShopCalendarWorkCtr)
  4. /// 路由前缀:/api/Production/shop-calendar/...
  5. ///
  6. /// 【租户安全边界】租户一律经 <see cref="AidopTenantScope.ResolveOrThrow"/> 从认证后 JWT 解析,
  7. /// 无有效租户即拒绝,不读前端 tenantId、无默认回退(原实现:类级 <c>[AllowAnonymous]</c> + 宽松解析,
  8. /// 未认证方可携带 <c>?tenantId=&lt;任意租户&gt;</c> 读写该租户数据)。
  9. /// </summary>
  10. [ApiDescriptionSettings(Order = 267, Description = "产线工作日历")]
  11. [Route("api/Production")]
  12. [NonUnify]
  13. public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
  14. {
  15. private readonly ISqlSugarClient _db;
  16. private readonly UserManager _userManager;
  17. public ShopCalendarWorkCtrService(ISqlSugarClient db, UserManager userManager)
  18. {
  19. _db = db;
  20. _userManager = userManager;
  21. }
  22. [DisplayName("产线工作日历列表")]
  23. [HttpGet("shop-calendar/list")]
  24. public async Task<object> GetList([FromQuery] ShopCalendarWorkCtrListInput input)
  25. {
  26. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  27. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  28. var where = "sc.IsActive = 1 AND sc.tenant_id = @TenantId";
  29. if (!string.IsNullOrWhiteSpace(input.ProdLine))
  30. {
  31. where += " AND sc.ProdLine = @ProdLine";
  32. pars.Add(new SugarParameter("@ProdLine", input.ProdLine.Trim()));
  33. }
  34. var orderClause = ResolveOrder(input.OrderBy, input.Sort);
  35. var offset = (input.Page - 1) * input.PageSize;
  36. var baseSql = $"""
  37. SELECT
  38. sc.ProdLine,
  39. sc.Ufld1 AS Descr,
  40. CASE sc.WeekDay
  41. WHEN 0 THEN '星期天'
  42. WHEN 1 THEN '星期一'
  43. WHEN 2 THEN '星期二'
  44. WHEN 3 THEN '星期三'
  45. WHEN 4 THEN '星期四'
  46. WHEN 5 THEN '星期五'
  47. WHEN 6 THEN '星期六'
  48. ELSE CAST(sc.WeekDay AS CHAR)
  49. END AS WeekDayName,
  50. REPLACE(CAST(CAST(sc.ShiftsStart1 AS DECIMAL(13,2)) AS CHAR), '.', ':') AS ShiftsStart1,
  51. sc.ShiftsHours1 AS ShiftsHours1,
  52. REPLACE(CAST(CAST(IFNULL(sc.ShiftsStart2,0) AS DECIMAL(13,2)) AS CHAR), '.', ':') AS ShiftsStart2,
  53. sc.ShiftsHours2 AS ShiftsHours2,
  54. sc.RecID AS Id,
  55. CASE WHEN sc.WeekDay = 0 THEN 7 ELSE sc.WeekDay END AS number,
  56. sc.WeekDay AS WeekDay,
  57. sc.`Domain` AS Domain,
  58. sc.Site AS Site,
  59. sc.WorkCtr AS WorkCtr
  60. FROM ShopCalendarWorkCtr sc
  61. WHERE {where}
  62. """;
  63. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({baseSql}) AS t", pars);
  64. var list = await _db.Ado.SqlQueryAsync<ShopCalendarListRow>(
  65. $"SELECT * FROM ({baseSql}) AS t ORDER BY {orderClause} LIMIT {input.PageSize} OFFSET {offset}", pars);
  66. return new { total, page = input.Page, pageSize = input.PageSize, list };
  67. }
  68. [DisplayName("生产线下拉(LineMaster)")]
  69. [HttpGet("shop-calendar/lines")]
  70. public async Task<object> GetLines([FromQuery] string? domain)
  71. {
  72. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  73. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  74. var where = "IsActive = 1 AND tenant_id = @TenantId";
  75. if (!string.IsNullOrWhiteSpace(domain))
  76. {
  77. where += " AND `Domain` = @Domain";
  78. pars.Add(new SugarParameter("@Domain", domain.Trim()));
  79. }
  80. var rows = await _db.Ado.SqlQueryAsync<LineKvRow>(
  81. $"SELECT DISTINCT `Line` AS Value, `Line` AS Label FROM LineMaster WHERE {where} ORDER BY `Line`", pars);
  82. return new { list = rows };
  83. }
  84. [DisplayName("产线工作日历详情")]
  85. [HttpGet("shop-calendar/{id:long}")]
  86. public async Task<object> GetDetail(long id)
  87. {
  88. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  89. var row = (await _db.Ado.SqlQueryAsync<ShopCalendarDetailRow>(
  90. """
  91. SELECT RecID AS Id, ProdLine, WeekDay, ShiftsStart1, ShiftsHours1, ShiftsStart2, ShiftsHours2,
  92. ShiftsStart3, ShiftsHours3, ShiftsStart4, ShiftsHours4,
  93. Ufld1 AS Descr, `Domain`, Site, WorkCtr, TeamType, AllowOverTime, IsWorkDay
  94. FROM ShopCalendarWorkCtr
  95. WHERE RecID = @Id AND tenant_id = @TenantId
  96. """,
  97. new List<SugarParameter> { new("@Id", id), new("@TenantId", tenantId) })).FirstOrDefault()
  98. ?? throw Oops.Oh("记录不存在");
  99. row.ShiftsStart1Text = DecimalHoursToTimeString(row.ShiftsStart1);
  100. row.ShiftsStart2Text = DecimalHoursToTimeString(row.ShiftsStart2);
  101. return row;
  102. }
  103. /// <summary>保存 POST /api/Production/shop-calendar/save</summary>
  104. [DisplayName("保存产线工作日历")]
  105. [HttpPost("shop-calendar/save")]
  106. public async Task<object> Save([FromBody] ShopCalendarWorkCtrSaveInput input)
  107. {
  108. var account = Truncate(_userManager.Account ?? "system", 8);
  109. var now = DateTime.Now;
  110. var domain = Truncate(input.Domain.Trim(), 8);
  111. var site = Truncate(input.Site.Trim(), 8);
  112. var prodLine = Truncate(input.ProdLine.Trim(), 8);
  113. var workCtr = string.IsNullOrWhiteSpace(input.WorkCtr) ? null : Truncate(input.WorkCtr.Trim(), 8);
  114. var ufld1 = string.IsNullOrWhiteSpace(input.Ufld1) ? null : Truncate(input.Ufld1.Trim(), 8);
  115. var s1 = NormalizeShiftDecimal(input.ShiftsStart1);
  116. var s2 = NormalizeShiftDecimal(input.ShiftsStart2);
  117. var h1 = input.ShiftsHours1 ?? 0m;
  118. var h2 = input.ShiftsHours2 ?? 0m;
  119. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  120. if (input.Id is null or 0)
  121. {
  122. var dup = await _db.Ado.GetIntAsync(
  123. """
  124. SELECT COUNT(*) FROM ShopCalendarWorkCtr
  125. WHERE IsActive = 1 AND ProdLine = @ProdLine AND WeekDay = @WeekDay AND tenant_id = @TenantId
  126. """,
  127. new List<SugarParameter>
  128. {
  129. new("@ProdLine", prodLine),
  130. new("@WeekDay", input.WeekDay),
  131. new("@TenantId", tenantId)
  132. });
  133. if (dup > 0)
  134. throw Oops.Oh("该生产线与星期组合已存在");
  135. await _db.Ado.ExecuteCommandAsync(
  136. """
  137. INSERT INTO ShopCalendarWorkCtr (
  138. `Domain`, `Site`, `ProdLine`, `WeekDay`, `WorkCtr`, `Ufld1`,
  139. ShiftsStart1, ShiftsHours1, ShiftsStart2, ShiftsHours2,
  140. ShiftsStart3, ShiftsHours3, ShiftsStart4, ShiftsHours4,
  141. AllowOverTime, IsChanged, IsWorkDay,
  142. IsActive, IsConfirm, BusinessID,
  143. CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id
  144. ) VALUES (
  145. @Domain, @Site, @ProdLine, @WeekDay, @WorkCtr, @Ufld1,
  146. @S1, @H1, @S2, @H2,
  147. 0, 0, 0, 0,
  148. 0, 0, 1,
  149. 1, 0, 0,
  150. @User, @Now, @User, @Now, @TenantId
  151. )
  152. """,
  153. new List<SugarParameter>
  154. {
  155. new("@Domain", domain),
  156. new("@Site", site),
  157. new("@ProdLine", prodLine),
  158. new("@WeekDay", input.WeekDay),
  159. new("@WorkCtr", (object?)workCtr ?? DBNull.Value),
  160. new("@Ufld1", (object?)ufld1 ?? DBNull.Value),
  161. new("@S1", s1),
  162. new("@H1", h1),
  163. new("@S2", s2),
  164. new("@H2", h2),
  165. new("@User", account),
  166. new("@Now", now),
  167. new("@TenantId", tenantId)
  168. });
  169. var newId = await _db.Ado.GetIntAsync("SELECT LAST_INSERT_ID()");
  170. return new { id = (long)newId, message = "新增成功" };
  171. }
  172. var n = await _db.Ado.ExecuteCommandAsync(
  173. """
  174. UPDATE ShopCalendarWorkCtr
  175. SET `Domain` = @Domain,
  176. `Site` = @Site,
  177. ProdLine = @ProdLine,
  178. WeekDay = @WeekDay,
  179. WorkCtr = @WorkCtr,
  180. Ufld1 = @Ufld1,
  181. ShiftsStart1 = @S1,
  182. ShiftsHours1 = @H1,
  183. ShiftsStart2 = @S2,
  184. ShiftsHours2 = @H2,
  185. UpdateUser = @User,
  186. UpdateTime = @Now
  187. WHERE RecID = @Id AND tenant_id = @TenantId
  188. """,
  189. new List<SugarParameter>
  190. {
  191. new("@Id", input.Id!.Value),
  192. new("@Domain", domain),
  193. new("@Site", site),
  194. new("@ProdLine", prodLine),
  195. new("@WeekDay", input.WeekDay),
  196. new("@WorkCtr", (object?)workCtr ?? DBNull.Value),
  197. new("@Ufld1", (object?)ufld1 ?? DBNull.Value),
  198. new("@S1", s1),
  199. new("@H1", h1),
  200. new("@S2", s2),
  201. new("@H2", h2),
  202. new("@User", account),
  203. new("@Now", now),
  204. new("@TenantId", tenantId)
  205. });
  206. if (n == 0)
  207. throw Oops.Oh("记录不存在或未变更");
  208. return new { id = input.Id, message = "保存成功" };
  209. }
  210. [DisplayName("删除产线工作日历")]
  211. [HttpPost("shop-calendar/delete/{id:long}")]
  212. public async Task<object> Delete(long id)
  213. {
  214. var tenantId = AidopTenantScope.ResolveOrThrow(_userManager);
  215. var n = await _db.Ado.ExecuteCommandAsync(
  216. "UPDATE ShopCalendarWorkCtr SET IsActive = 0, UpdateTime = @Now WHERE RecID = @Id AND tenant_id = @TenantId",
  217. new List<SugarParameter> { new("@Id", id), new("@Now", DateTime.Now), new("@TenantId", tenantId) });
  218. if (n == 0)
  219. throw Oops.Oh("记录不存在");
  220. return new { message = "已删除" };
  221. }
  222. private static string Truncate(string s, int maxLen)
  223. {
  224. if (string.IsNullOrEmpty(s)) return s;
  225. return s.Length <= maxLen ? s : s[..maxLen];
  226. }
  227. private static string ResolveOrder(string? orderBy, string? sort)
  228. {
  229. var desc = string.Equals(sort?.Trim(), "desc", StringComparison.OrdinalIgnoreCase);
  230. var dir = desc ? "DESC" : "ASC";
  231. var key = orderBy?.Trim().ToLowerInvariant();
  232. var col = key switch
  233. {
  234. "prodline" => "t.ProdLine",
  235. "weekdayname" => "t.number",
  236. _ => "t.number"
  237. };
  238. return $"{col} {dir}, t.Id";
  239. }
  240. private static decimal? ParseTimeToDecimal(string? s)
  241. {
  242. if (string.IsNullOrWhiteSpace(s)) return null;
  243. var p = s.Trim().Split(':');
  244. if (p.Length < 2 || !int.TryParse(p[0], out var h) || !int.TryParse(p[1], out var m))
  245. return null;
  246. return h + m / 60m;
  247. }
  248. private static decimal NormalizeShiftDecimal(decimal? value)
  249. {
  250. if (value is null) return 0m;
  251. // DB 列是 decimal(10,5),避免循环小数导致超精度写入报错。
  252. return Math.Round(value.Value, 5, MidpointRounding.AwayFromZero);
  253. }
  254. private static string? DecimalHoursToTimeString(decimal? d)
  255. {
  256. if (d is null || d.Value == 0) return null;
  257. var total = (double)d.Value;
  258. var h = (int)Math.Floor(total);
  259. var m = (int)Math.Round((total - h) * 60);
  260. if (m >= 60) { h++; m = 0; }
  261. return $"{h:D2}:{m:D2}";
  262. }
  263. private sealed class ShopCalendarListRow
  264. {
  265. public string? ProdLine { get; set; }
  266. public string? Descr { get; set; }
  267. public string? WeekDayName { get; set; }
  268. public string? ShiftsStart1 { get; set; }
  269. public decimal? ShiftsHours1 { get; set; }
  270. public string? ShiftsStart2 { get; set; }
  271. public decimal? ShiftsHours2 { get; set; }
  272. public long Id { get; set; }
  273. public int number { get; set; }
  274. public int WeekDay { get; set; }
  275. public string? Domain { get; set; }
  276. public string? Site { get; set; }
  277. public string? WorkCtr { get; set; }
  278. }
  279. private sealed class LineKvRow
  280. {
  281. public string? Value { get; set; }
  282. public string? Label { get; set; }
  283. }
  284. private sealed class ShopCalendarDetailRow
  285. {
  286. public long Id { get; set; }
  287. public string? ProdLine { get; set; }
  288. public int WeekDay { get; set; }
  289. public decimal? ShiftsStart1 { get; set; }
  290. public decimal? ShiftsHours1 { get; set; }
  291. public decimal? ShiftsStart2 { get; set; }
  292. public decimal? ShiftsHours2 { get; set; }
  293. public decimal? ShiftsStart3 { get; set; }
  294. public decimal? ShiftsHours3 { get; set; }
  295. public decimal? ShiftsStart4 { get; set; }
  296. public decimal? ShiftsHours4 { get; set; }
  297. public string? Descr { get; set; }
  298. public string? Domain { get; set; }
  299. public string? Site { get; set; }
  300. public string? WorkCtr { get; set; }
  301. public string? TeamType { get; set; }
  302. public int? AllowOverTime { get; set; }
  303. public int? IsWorkDay { get; set; }
  304. public string? ShiftsStart1Text { get; set; }
  305. public string? ShiftsStart2Text { get; set; }
  306. }
  307. }