SystemDatabaseService.cs 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  1. using SqlSugar;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Threading.Tasks;
  7. using TeamAAS_VP.Interfaces;
  8. using TeamAAS_VP.Models;
  9. namespace TeamAAS_VP.Data
  10. {
  11. /// <summary>
  12. /// 系统数据库服务,基于 <see cref="ISqlSugarClient"/> 提供对应用数据的访问。
  13. /// 包含用户增删改查、用户身份验证以及生产记录的写入与统计功能。
  14. /// </summary>
  15. public class SystemDatabaseService : ISystemDatabaseService
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private User CurrentUser { get; set; }
  19. /// <summary>
  20. /// 使用指定的 SqlSugar 客户端构造数据库服务实例。
  21. /// </summary>
  22. /// <param name="db">已配置的 <see cref="ISqlSugarClient"/> 实例。</param>
  23. public SystemDatabaseService(ISqlSugarClient db)
  24. {
  25. _db = db;
  26. }
  27. #region 用户
  28. // User CRUD
  29. /// <summary>
  30. /// 添加新用户到数据库。
  31. /// 对于 SqlSugar,User.Id 为数据库自增值,因此在此方法中不应设定 Id。
  32. /// </summary>
  33. /// <param name="user">要添加的用户对象,不能为空。</param>
  34. /// <returns>返回传入的用户对象(Id 由数据库生成后会回写到该对象)。</returns>
  35. /// <exception cref="ArgumentNullException">当 <paramref name="user"/> 为 null 时抛出。</exception>
  36. public async Task<User> AddUserAsync(User user)
  37. {
  38. if (user == null) throw new ArgumentNullException(nameof(user));
  39. // for SqlSugar User.Id is int identity, so do not set it here
  40. await _db.Insertable(user).ExecuteCommandAsync();
  41. return user;
  42. }
  43. /// <summary>
  44. /// 根据用户 Id 删除用户。
  45. /// </summary>
  46. /// <param name="userId">要删除的用户 Id。</param>
  47. /// <returns>如果删除了至少一条记录则返回 true,否则返回 false。</returns>
  48. public async Task<bool> DeleteUserAsync(int userId)
  49. {
  50. var r = await _db.Deleteable<User>().In(userId).ExecuteCommandAsync();
  51. return r > 0;
  52. }
  53. /// <summary>
  54. /// 获取所有用户。
  55. /// </summary>
  56. /// <returns>用户列表的异步枚举。</returns>
  57. public async Task<IEnumerable<User>> GetAllUsersAsync()
  58. {
  59. return await _db.Queryable<User>().ToListAsync();
  60. }
  61. /// <summary>
  62. /// 根据用户 Id 查询单个用户。
  63. /// </summary>
  64. /// <param name="userId">用户 Id。</param>
  65. /// <returns>匹配的用户,如果未找到则返回 null(取决于 SqlSugar 的行为)。</returns>
  66. public async Task<User> GetUserByIdAsync(int userId)
  67. {
  68. return await _db.Queryable<User>().InSingleAsync(userId);
  69. }
  70. /// <summary>
  71. /// 更新用户信息并返回更新后的对象。
  72. /// </summary>
  73. /// <param name="user">要更新的用户对象,不能为空。</param>
  74. /// <returns>返回传入的用户对象。</returns>
  75. /// <exception cref="ArgumentNullException">当 <paramref name="user"/> 为 null 时抛出。</exception>
  76. public async Task<User> UpdateUserAsync(User user)
  77. {
  78. if (user == null) throw new ArgumentNullException(nameof(user));
  79. var r = await _db.Updateable(user).ExecuteCommandAsync();
  80. return user;
  81. }
  82. // User authentication
  83. /// <summary>
  84. /// 验证用户名和密码并返回匹配的用户。
  85. /// 注意:当前实现直接以明文密码比较,请根据需要在调用侧或更高层添加哈希/盐策略。
  86. /// </summary>
  87. /// <param name="username">用户名,不能为空或空字符串。</param>
  88. /// <param name="password">密码,不能为空或空字符串。</param>
  89. /// <returns>匹配的用户对象,如果未找到则可能抛出或返回 null(取决于 SqlSugar 的 FirstAsync 行为)。</returns>
  90. /// <exception cref="ArgumentException">当 <paramref name="username"/> 或 <paramref name="password"/> 为空时抛出。</exception>
  91. public async Task<User> AuthenticateUserAsync(string username, string password)
  92. {
  93. if (string.IsNullOrEmpty(username)) throw new ArgumentException("username is required", nameof(username));
  94. if (string.IsNullOrEmpty(password)) throw new ArgumentException("password is required", nameof(password));
  95. return await _db.Queryable<User>().Where(u => u.UserName == username && u.UserPassword == password).FirstAsync();
  96. }
  97. // User login / current user helpers
  98. /// <summary>
  99. /// 将指定用户设为当前用户(写入一条成功的登录记录)。
  100. /// </summary>
  101. /// <param name="userId">要设为当前用户的用户 Id。</param>
  102. public async Task SetCurrentUserAsync(int userId)
  103. {
  104. // 验证用户存在
  105. var user = await GetUserByIdAsync(userId);
  106. if (user == null)
  107. {
  108. throw new ArgumentException($"User with id {userId} not found", nameof(userId));
  109. }
  110. CurrentUser = user;
  111. // 记录一条成功的登录记录
  112. var rec = new UserLoginRecord
  113. {
  114. UserId = userId,
  115. Success = true,
  116. Time = DateTime.UtcNow,
  117. Message = "Set as current user"
  118. };
  119. await RecordUserLoginAsync(rec);
  120. }
  121. /// <summary>
  122. /// 将指定用户设为当前用户(会写入一条登录记录,标记为成功)
  123. /// </summary>
  124. /// <param name="user"></param>
  125. /// <returns></returns>
  126. public async Task SetCurrentUserAsync(User user)
  127. {
  128. CurrentUser = user;
  129. // 记录一条成功的登录记录
  130. var rec = new UserLoginRecord
  131. {
  132. UserId = user.Id,
  133. Success = true,
  134. Time = DateTime.UtcNow,
  135. Message = user.UserName
  136. };
  137. await RecordUserLoginAsync(rec);
  138. }
  139. /// <summary>
  140. /// 获取最近一次成功登录的用户(视为当前用户)。
  141. /// </summary>
  142. /// <returns>若存在成功登录记录则返回对应用户,否则返回 null。</returns>
  143. public Task<User> GetCurrentUserAsync()
  144. {
  145. return Task.FromResult(GetCurrentUser());
  146. }
  147. /// <summary>
  148. /// 获取当前用户
  149. /// </summary>
  150. /// <returns></returns>
  151. public User GetCurrentUser()
  152. {
  153. //var recs = _db.Queryable<UserLoginRecord>()
  154. // .Where(r => r.Success == true)
  155. // .OrderBy(r => r.Time, OrderByType.Desc)
  156. // .Take(1)
  157. // .ToList();
  158. //var rec = recs.FirstOrDefault();
  159. ////如果没有登录记录则返回一个默认的用户,即第一个用户
  160. //if (rec == null) return _db.Queryable<User>().First();
  161. //return _db.Queryable<User>().InSingle(rec.UserId);
  162. if (CurrentUser == null)
  163. {
  164. CurrentUser = _db.Queryable<User>().ToList().FirstOrDefault();
  165. }
  166. return CurrentUser;
  167. }
  168. /// <summary>
  169. /// 记录一条用户登录记录(成功或失败)。
  170. /// </summary>
  171. /// <param name="record">登录记录对象,不能为空。</param>
  172. public async Task RecordUserLoginAsync(UserLoginRecord record)
  173. {
  174. if (record == null) throw new ArgumentNullException(nameof(record));
  175. if (record.Time == default(DateTime)) record.Time = DateTime.UtcNow;
  176. await _db.Insertable(record).ExecuteCommandAsync();
  177. }
  178. /// <summary>
  179. /// 获取指定用户最近的一条登录记录(无论是否成功)。
  180. /// </summary>
  181. /// <param name="userId">用户 Id。</param>
  182. /// <returns>最近的一条登录记录或 null。</returns>
  183. public async Task<UserLoginRecord> GetLastUserLoginRecordAsync(int userId)
  184. {
  185. var recs = await _db.Queryable<UserLoginRecord>()
  186. .Where(r => r.UserId == userId)
  187. .OrderBy(r => r.Time, OrderByType.Desc)
  188. .Take(1)
  189. .ToListAsync();
  190. return recs.FirstOrDefault();
  191. }
  192. /// <summary>
  193. /// 将指定用户名标记为“记住”,并取消其他用户的记住标志。
  194. /// </summary>
  195. /// <param name="username">要记住的用户名。</param>
  196. public async Task SetRememberUserAsync(string username)
  197. {
  198. if (string.IsNullOrEmpty(username)) throw new ArgumentException("username is required", nameof(username));
  199. // 使用事务先清除所有记住标志,再为指定用户设置记住
  200. try
  201. {
  202. _db.Ado.BeginTran();
  203. // 清除所有
  204. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = false }).Where("1=1").ExecuteCommandAsync();
  205. // 设置指定用户为记住
  206. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = true }).Where(u => u.UserName == username).ExecuteCommandAsync();
  207. _db.Ado.CommitTran();
  208. }
  209. catch
  210. {
  211. try { _db.Ado.RollbackTran(); } catch { }
  212. throw;
  213. }
  214. }
  215. /// <summary>
  216. /// 清除所有用户的记住标志。
  217. /// </summary>
  218. public async Task ClearRememberUserAsync()
  219. {
  220. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = false }).ExecuteCommandAsync();
  221. }
  222. /// <summary>
  223. /// 获取当前被标记为记住的用户(如果存在)。
  224. /// </summary>
  225. /// <returns>被记住的用户或 null。</returns>
  226. public async Task<User> GetRememberedUserAsync()
  227. {
  228. return await _db.Queryable<User>().Where(u => u.IsRemember == true).FirstAsync();
  229. }
  230. #endregion
  231. #region 生产记录
  232. // Production records
  233. /// <summary>
  234. /// 记录一次生产事件。如果 <see cref="ProductionRecord.Id"/> 为 <see cref="Guid.Empty"/>
  235. /// 则会自动生成新的 Guid。
  236. /// </summary>
  237. /// <param name="record">生产记录对象,不能为空。</param>
  238. /// <returns>异步任务。</returns>
  239. /// <exception cref="ArgumentNullException">当 <paramref name="record"/> 为 null 时抛出。</exception>
  240. public async Task RecordProductionAsync(ProductionRecord record)
  241. {
  242. if (record == null) throw new ArgumentNullException(nameof(record));
  243. // 如果未指定产生该记录的用户名,尝试使用当前登录用户
  244. if (string.IsNullOrEmpty(record.UserName))
  245. {
  246. try
  247. {
  248. var current = await GetCurrentUserAsync();
  249. if (current != null)
  250. {
  251. record.UserName = current.UserName;
  252. }
  253. }
  254. catch
  255. {
  256. // 忽略获取当前用户时的错误,继续保存记录(保守策略)
  257. }
  258. }
  259. record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
  260. await _db.Insertable(record).ExecuteCommandAsync();
  261. }
  262. /// <summary>
  263. /// 重置生产记录。若未传入 <paramref name="productName"/>
  264. /// 则清空所有生产记录;否则仅删除指定产品名的记录。
  265. /// </summary>
  266. /// <param name="productName">可选的产品名过滤。</param>
  267. /// <returns>异步任务。</returns>
  268. public async Task ResetProductionAsync(string productName = null)
  269. {
  270. if (string.IsNullOrEmpty(productName))
  271. {
  272. await _db.Deleteable<ProductionRecord>().Where("1=1").ExecuteCommandAsync();
  273. }
  274. else
  275. {
  276. await _db.Deleteable<ProductionRecord>().Where(r => r.ProductName == productName).ExecuteCommandAsync();
  277. }
  278. }
  279. /// <summary>
  280. /// 获取指定产品(和可选类别)在数据库中累计的产量总和(仅统计 Quantity &gt; 0 的记录)。
  281. /// </summary>
  282. /// <param name="productName">产品名,不能为空或空字符串。</param>
  283. /// <param name="categories">可选的类别集合,用于过滤。</param>
  284. /// <returns>指定条件下的产量总和(整数)。</returns>
  285. /// <exception cref="ArgumentException">当 <paramref name="productName"/> 为空时抛出。</exception>
  286. public async Task<int> GetTotalProductionAsync(string productName, IEnumerable<string> categories = null)
  287. {
  288. if (string.IsNullOrEmpty(productName)) throw new ArgumentException("productName is required", nameof(productName));
  289. var q = _db.Queryable<ProductionRecord>().Where(r => r.ProductName == productName && r.Quantity > 0);
  290. if (categories != null && categories.Any())
  291. {
  292. q = q.Where(r => categories.Contains(r.Category));
  293. }
  294. var sum = await q.SumAsync<int>(r => r.Quantity);
  295. return sum;
  296. }
  297. /// <summary>
  298. /// 获取生产统计结果,按指定时间范围、产品名和可选类别筛选,
  299. /// 并根据 <see cref="ProductionStatQuery.GroupBy"/>(hour/day/week/month/total)进行分组聚合。
  300. /// </summary>
  301. /// <param name="query">生产统计查询对象,不能为空,必须包含 ProductName/Start/End 等。</param>
  302. /// <returns>按指定分组粒度返回的统计结果集合。</returns>
  303. /// <exception cref="ArgumentNullException">当 <paramref name="query"/> 为 null 时抛出。</exception>
  304. public async Task<IEnumerable<ProductionStatResult>> GetProductionStatsAsync(ProductionStatQuery query)
  305. {
  306. if (query == null) throw new ArgumentNullException(nameof(query));
  307. if (string.IsNullOrEmpty(query.ProductName)) throw new ArgumentException("ProductName is required in query", nameof(query));
  308. var q = _db.Queryable<ProductionRecord>().Where(r => r.ProductName == query.ProductName && r.Timestamp >= query.Start && r.Timestamp < query.End);
  309. if (query.Categories != null && query.Categories.Any())
  310. {
  311. q = q.Where(r => query.Categories.Contains(r.Category));
  312. }
  313. var list = await q.ToListAsync();
  314. var results = new List<ProductionStatResult>();
  315. switch ((query.GroupBy ?? "total").ToLower())
  316. {
  317. case "hour":
  318. // group by hour
  319. var hours = list.GroupBy(r => new DateTime(r.Timestamp.Year, r.Timestamp.Month, r.Timestamp.Day, r.Timestamp.Hour, 0, 0))
  320. .OrderBy(g => g.Key);
  321. foreach (var g in hours)
  322. {
  323. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddHours(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  324. }
  325. break;
  326. case "day":
  327. var days = list.GroupBy(r => r.Timestamp.Date).OrderBy(g => g.Key);
  328. foreach (var g in days)
  329. {
  330. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddDays(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  331. }
  332. break;
  333. case "week":
  334. // ISO week grouping by week start (Monday)
  335. var weeks = list.GroupBy(r => FirstDayOfWeek(r.Timestamp)).OrderBy(g => g.Key);
  336. foreach (var g in weeks)
  337. {
  338. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddDays(7), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  339. }
  340. break;
  341. case "month":
  342. var months = list.GroupBy(r => new DateTime(r.Timestamp.Year, r.Timestamp.Month, 1)).OrderBy(g => g.Key);
  343. foreach (var g in months)
  344. {
  345. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddMonths(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  346. }
  347. break;
  348. default:
  349. results.Add(new ProductionStatResult { PeriodStart = query.Start, PeriodEnd = query.End, Quantity = list.Sum(x => x.Quantity), ProductName = query.ProductName });
  350. break;
  351. }
  352. return results;
  353. }
  354. /// <summary>
  355. /// 获取所有已知分类相加起来的今天的产能
  356. /// </summary>
  357. /// <param name="productName"></param>
  358. /// <returns></returns>
  359. public async Task<int> GetTodayProductionAsync(string productName)
  360. {
  361. //今天的起始时间
  362. var todayStart = DateTime.UtcNow.Date;
  363. //今天的结束时间
  364. var todayEnd = DateTime.UtcNow.Date;
  365. todayEnd = todayEnd.AddDays(1);
  366. //按照产品名称和时间范围查询生产记录的总数
  367. //var total = await _db.Queryable<ProductionRecord>()
  368. // .Where(r => r.ProductName == productName && r.Timestamp >= todayStart && r.Timestamp < todayEnd)
  369. // .SumAsync(r => r.Quantity);
  370. var total = await _db.Queryable<ProductionRecord>()
  371. .Where(r => r.ProductName == productName && r.Timestamp >= todayStart && r.Timestamp < todayEnd)
  372. .Select(r => r.ProductCode)
  373. .Distinct()
  374. .CountAsync();
  375. return total;
  376. }
  377. /// <summary>
  378. /// 获取所有已知分类相加起来的当前时间往前的一个小时的产能
  379. /// </summary>
  380. /// <param name="productName"></param>
  381. /// <returns></returns>
  382. public async Task<int> GetLastHourProductionAsync(string productName)
  383. {
  384. var lastHourStart = DateTime.UtcNow.AddHours(-1);
  385. var lastHourEnd = DateTime.UtcNow;
  386. //按照产品名称和时间范围查询生产记录的总数
  387. //var total = await _db.Queryable<ProductionRecord>()
  388. // .Where(r => r.ProductName == productName && r.Timestamp >= lastHourStart && r.Timestamp < lastHourEnd)
  389. // .SumAsync(r => r.Quantity);
  390. var total = await _db.Queryable<ProductionRecord>()
  391. .Where(r => r.ProductName == productName && r.Timestamp >= lastHourStart && r.Timestamp < lastHourEnd)
  392. .Select(r => r.ProductCode)
  393. .Distinct()
  394. .CountAsync();
  395. return total;
  396. }
  397. /// <summary>
  398. /// 获取今天ng的数量
  399. /// </summary>
  400. /// <param name="productName"></param>
  401. /// <returns></returns>
  402. public async Task<int> GetNGProductionAsync(string productName)
  403. {
  404. //今天的起始时间
  405. var todayStart = DateTime.Now.Date;
  406. //今天的结束时间
  407. var todayEnd = DateTime.Now.Date;
  408. todayEnd = todayEnd.AddDays(1);
  409. //按照产品名称和时间范围查询生产记录的总数
  410. //var total = await _db.Queryable<ProductionRecord>()
  411. // .Where(r => r.ProductName == productName && r.Timestamp >= todayStart && r.Timestamp < todayEnd && r.Category == "NG")
  412. // .SumAsync(r => r.Quantity);
  413. var total = await _db.Queryable<ProductionRecord>()
  414. .Where(r => r.ProductName == productName && r.Timestamp >= todayStart && r.Timestamp < todayEnd && r.Category == "NG")
  415. .Select(r => r.ProductCode)
  416. .Distinct()
  417. .CountAsync();
  418. return total;
  419. }
  420. /// <summary>
  421. /// 获取当前月的产能
  422. /// </summary>
  423. /// <param name="productName"></param>
  424. /// <returns></returns>
  425. public Task<int> GetCurrentMonthProductionAsync(string productName)
  426. {
  427. var now = DateTime.UtcNow;
  428. var monthStart = new DateTime(now.Year, now.Month, 1);
  429. var monthEnd = monthStart.AddMonths(1);
  430. //按照产品名称和时间范围查询生产记录的总数
  431. return _db.Queryable<ProductionRecord>()
  432. .Where(r => r.ProductName == productName && r.Timestamp >= monthStart && r.Timestamp < monthEnd)
  433. .SumAsync(r => r.Quantity);
  434. }
  435. /// <summary>
  436. /// 获取当前周的产能
  437. /// </summary>
  438. /// <param name="productName"></param>
  439. /// <returns></returns>
  440. public Task<int> GetCurrentWeekProductionAsync(string productName)
  441. {
  442. var now = DateTime.UtcNow;
  443. var weekStart = FirstDayOfWeek(now);
  444. var weekEnd = weekStart.AddDays(7);
  445. //按照产品名称和时间范围查询生产记录的总数
  446. return _db.Queryable<ProductionRecord>()
  447. .Where(r => r.ProductName == productName && r.Timestamp >= weekStart && r.Timestamp < weekEnd)
  448. .SumAsync(r => r.Quantity);
  449. }
  450. /// <summary>
  451. /// 计算给定日期所属周的第一天(以星期一为一周的第一天)。
  452. /// 返回结果为日期的 Date 部分(时间为 00:00:00)。
  453. /// </summary>
  454. /// <param name="dt">输入日期。</param>
  455. /// <returns>该日期对应周的周一日期(Date 部分)。</returns>
  456. private DateTime FirstDayOfWeek(DateTime dt)
  457. {
  458. var diff = (7 + (dt.DayOfWeek - DayOfWeek.Monday)) % 7;
  459. return dt.Date.AddDays(-1 * diff);
  460. }
  461. /// <summary>
  462. /// 获取指定产品名称的总产能
  463. /// </summary>
  464. /// <param name="productName"></param>
  465. /// <returns></returns>
  466. public Task<int> GetOverallProductionAsync(string productName)
  467. {
  468. return _db.Queryable<ProductionRecord>()
  469. .Where(r => r.ProductName == productName)
  470. .SumAsync(r => r.Quantity);
  471. }
  472. /// <summary>
  473. /// 获取指定产品的所有已知分类。
  474. /// </summary>
  475. /// <param name="productName">要查询分类的产品名称。</param>
  476. /// <returns>
  477. /// 一个字符串集合,包含该产品的所有分类名称;如果没有分类,返回空集合或 null(建议返回空集合)。
  478. /// </returns>
  479. public async Task<IEnumerable<string>> GetProductCategoriesAsync(string productName)
  480. {
  481. var categories = await _db.Queryable<ProductionRecord>()
  482. .Where(r => r.ProductName == productName)
  483. .Select(r => r.Category)
  484. .Distinct()
  485. .ToListAsync();
  486. return categories;
  487. }
  488. /// <summary>
  489. /// 获取指定产品名称的所有记录按照分类的产能统计
  490. /// </summary>
  491. /// <param name="productName"></param>
  492. /// <returns></returns>
  493. public async Task<Dictionary<string, int>> GetProductionByCategoriesAsync(string productName)
  494. {
  495. var list = await _db.Queryable<ProductionRecord>()
  496. .Where(r => r.ProductName == productName)
  497. .ToListAsync();
  498. var dict = list.GroupBy(r => r.Category)
  499. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  500. return dict;
  501. }
  502. /// <summary>
  503. /// 获取指定产品名称的一个月内按照每天统计对应的类别和数量
  504. /// </summary>
  505. /// <param name="productName"></param>
  506. /// <returns></returns>
  507. public async Task<Dictionary<DateTime, Dictionary<string, int>>> GetMonthlyProductionByDayAndCategoryAsync(string productName)
  508. {
  509. var now = DateTime.UtcNow;
  510. var monthStart = new DateTime(now.Year, now.Month, 1);
  511. var monthEnd = monthStart.AddMonths(1);
  512. var list = await _db.Queryable<ProductionRecord>()
  513. .Where(r => r.ProductName == productName && r.Timestamp >= monthStart && r.Timestamp < monthEnd)
  514. .ToListAsync();
  515. var dict = new Dictionary<DateTime, Dictionary<string, int>>();
  516. var days = list.GroupBy(r => r.Timestamp.Date).OrderBy(g => g.Key);
  517. foreach (var dayGroup in days)
  518. {
  519. var categoryDict = dayGroup.GroupBy(r => r.Category)
  520. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  521. dict[dayGroup.Key] = categoryDict;
  522. }
  523. return dict;
  524. }
  525. /// <summary>
  526. /// 获取指定产品名称的一个星期内按照每天统计对应的类别和数量
  527. /// </summary>
  528. /// <param name="productName"></param>
  529. /// <returns></returns>
  530. public async Task<Dictionary<int, Dictionary<string, int>>> GetWeeklyProductionByDayAndCategoryAsync(string productName)
  531. {
  532. var now = DateTime.UtcNow;
  533. var weekStart = FirstDayOfWeek(now);
  534. var weekEnd = weekStart.AddDays(7);
  535. var list = await _db.Queryable<ProductionRecord>()
  536. .Where(r => r.ProductName == productName && r.Timestamp >= weekStart && r.Timestamp < weekEnd)
  537. .ToListAsync();
  538. var dict = new Dictionary<int, Dictionary<string, int>>();
  539. var days = list.GroupBy(r => r.Timestamp.DayOfWeek).OrderBy(g => g.Key);
  540. foreach (var dayGroup in days)
  541. {
  542. var categoryDict = dayGroup.GroupBy(r => r.Category)
  543. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  544. dict[(int)dayGroup.Key] = categoryDict;
  545. }
  546. return dict;
  547. }
  548. /// <summary>
  549. /// 获取指定产品名称的当天内按照每小时统计对应的类别和数量
  550. /// </summary>
  551. /// <param name="productName"></param>
  552. /// <returns></returns>
  553. public async Task<Dictionary<int, Dictionary<string, int>>> GetDailyProductionByHourAndCategoryAsync(string productName)
  554. {
  555. var now = DateTime.UtcNow;
  556. var dayStart = now.Date;
  557. var dayEnd = dayStart.AddDays(1);
  558. var list = await _db.Queryable<ProductionRecord>()
  559. .Where(r => r.ProductName == productName && r.Timestamp >= dayStart && r.Timestamp < dayEnd)
  560. .ToListAsync();
  561. var dict = new Dictionary<int, Dictionary<string, int>>();
  562. var hours = list.GroupBy(r => r.Timestamp.Hour).OrderBy(g => g.Key);
  563. foreach (var hourGroup in hours)
  564. {
  565. var categoryDict = hourGroup.GroupBy(r => r.Category)
  566. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  567. dict[hourGroup.Key] = categoryDict;
  568. }
  569. return dict;
  570. }
  571. /// <summary>
  572. /// 分页查询生产记录
  573. /// </summary>
  574. public async Task<(IEnumerable<ProductionRecord> Items, int TotalCount)> QueryProductionRecordsPagedAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productName = null, string category = null)
  575. {
  576. if (pageIndex < 1) pageIndex = 1;
  577. if (pageSize < 1) pageSize = 20;
  578. var q = _db.Queryable<ProductionRecord>().Where(r => r.Timestamp >= start && r.Timestamp < end);
  579. if (!string.IsNullOrEmpty(productName)) q = q.Where(r => r.ProductName == productName);
  580. if (!string.IsNullOrEmpty(category)) q = q.Where(r => r.Category == category);
  581. var total = await q.CountAsync();
  582. var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
  583. return (list, total);
  584. }
  585. #endregion
  586. #region 报警记录
  587. // Alarm Records
  588. /// <summary>
  589. /// 记录一条报警记录。
  590. /// </summary>
  591. /// <param name="alarm">报警记录对象,不能为空。</param>
  592. public async Task RecordAlarmAsync(AlarmRecord alarm)
  593. {
  594. if (alarm == null) throw new ArgumentNullException(nameof(alarm));
  595. if (alarm.Time == default(DateTime)) alarm.Time = DateTime.UtcNow;
  596. await _db.Insertable(alarm).ExecuteCommandAsync();
  597. }
  598. /// <summary>
  599. /// 按时间范围和可选条件查询报警记录。
  600. /// </summary>
  601. /// <param name="start">起始时间(含)。</param>
  602. /// <param name="end">结束时间(不含)。</param>
  603. /// <param name="code">可选的报警编号过滤。</param>
  604. /// <param name="source">可选的报警来源过滤。</param>
  605. /// <returns>满足条件的报警记录集合。</returns>
  606. public async Task<IEnumerable<AlarmRecord>> QueryAlarmsAsync(DateTime start, DateTime end, string code = null, string source = null)
  607. {
  608. var q = _db.Queryable<AlarmRecord>().Where(a => a.Time >= start && a.Time < end);
  609. if (!string.IsNullOrEmpty(code)) q = q.Where(a => a.Code == code);
  610. if (!string.IsNullOrEmpty(source)) q = q.Where(a => a.Source == source);
  611. return await q.OrderBy(a => a.Time, OrderByType.Desc).ToListAsync();
  612. }
  613. #endregion
  614. #region 拍照记录
  615. /// <summary>
  616. /// 记录产品拍照结果
  617. /// </summary>
  618. /// <param name="record"></param>
  619. /// <returns></returns>
  620. /// <exception cref="ArgumentNullException"></exception>
  621. public async Task RecordPhotoCaptureAsync(PhotoCaptureRecord record)
  622. {
  623. if (record == null) throw new ArgumentNullException(nameof(record));
  624. if (record.CaptureTime == default(DateTime)) record.CaptureTime = DateTime.UtcNow;
  625. record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
  626. await _db.Insertable(record).ExecuteCommandAsync();
  627. }
  628. /// <summary>
  629. /// 查询拍照记录,支持按配方名、产品SN和时间段过滤,参数均为可选。
  630. /// </summary>
  631. /// <param name="recipeName"></param>
  632. /// <param name="cameraName"></param>
  633. /// <param name="productSN"></param>
  634. /// <param name="start"></param>
  635. /// <param name="end"></param>
  636. /// <returns></returns>
  637. public async Task<IEnumerable<PhotoCaptureRecord>> QueryPhotoCapturesAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null)
  638. {
  639. var q = _db.Queryable<PhotoCaptureRecord>();
  640. if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
  641. if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
  642. if (start.HasValue) q = q.Where(r => r.CaptureTime >= start.Value);
  643. if (end.HasValue) q = q.Where(r => r.CaptureTime <= end.Value);
  644. var list = await q.OrderBy(r => r.CaptureTime, OrderByType.Desc).ToListAsync();
  645. return list;
  646. }
  647. /// <summary>
  648. /// 分页查询拍照记录
  649. /// </summary>
  650. public async Task<(IEnumerable<PhotoCaptureRecord> Items, int TotalCount)> QueryPhotoCapturesPagedAsync(string recipeName = null, string cameraName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20)
  651. {
  652. if (pageIndex < 1) pageIndex = 1;
  653. if (pageSize < 1) pageSize = 20;
  654. var q = _db.Queryable<PhotoCaptureRecord>();
  655. if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
  656. if (!string.IsNullOrEmpty(cameraName)) q = q.Where(r => r.CameraName == cameraName);
  657. if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
  658. if (start.HasValue) q = q.Where(r => r.CaptureTime >= start.Value);
  659. if (end.HasValue) q = q.Where(r => r.CaptureTime <= end.Value);
  660. var total = await q.CountAsync();
  661. var list = await q.OrderBy(r => r.CaptureTime, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
  662. return (list, total);
  663. }
  664. #endregion
  665. #region 组装过程记录
  666. /// <summary>
  667. /// 记录一次组装过程记录
  668. /// </summary>
  669. /// <param name="record">组装记录对象</param>
  670. public async Task RecordAssemblyAsync(AssemblyRecord record)
  671. {
  672. if (record == null) throw new ArgumentNullException(nameof(record));
  673. if (record.Timestamp == default(DateTime)) record.Timestamp = DateTime.UtcNow;
  674. record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
  675. await _db.Insertable(record).ExecuteCommandAsync();
  676. }
  677. /// <summary>
  678. /// 查询最近20条记录 [SN|压力] 格式
  679. /// </summary>
  680. public async Task<List<string>> GetLatest20AssemblyPressureAsync(int deviceId)
  681. {
  682. var records = await _db.Queryable<AssemblyRecord>()
  683. .Where(r => r.DeviceId == deviceId)
  684. .OrderByDescending(r => r.Timestamp)
  685. .Take(20)
  686. .Select(r => new { r.AssemblyPressure, r.ProductSN }) // 只取需要的字段
  687. .ToListAsync();
  688. // 格式:SN + "|" + 压力
  689. return records.Select(r => $"{r.AssemblyPressure}|{r.ProductSN}").ToList();
  690. }
  691. /// <summary>
  692. /// 查询最近20条相机拍照检测记录结果值
  693. /// </summary>
  694. /// <param name="ProductName">产品型号</param>
  695. /// <param name="ProcedureName">检测工序</param>
  696. /// <param name="resultFieldName">拍照结果字段名(例:_MeasureValue)</param>
  697. /// <returns>最近20条记录检测数值列表(按时间倒序,最新在前)</returns>
  698. public async Task<List<double>> GetLatest20CameraResultAsync(string ProductName, string ProcedureName)
  699. {
  700. const string gapFieldName = "_gap";
  701. var records = DatabaseHelper.GetLast20CameraRecords(ProductName, ProcedureName);
  702. var gapList = new List<double>();
  703. foreach (var record in records)
  704. {
  705. // 自动从字典里取 _gap 字段并转 double
  706. if (record.TryGetValue(gapFieldName, out var valObj)
  707. && double.TryParse(valObj?.ToString(), out double gapValue))
  708. {
  709. gapList.Add(gapValue);
  710. }
  711. }
  712. // 保持和你方法一致的异步返回格式
  713. return await Task.FromResult(gapList);
  714. }
  715. /// <summary>
  716. /// 查询组装记录,支持按配方名、产品序列号和时间范围过滤
  717. /// </summary>
  718. public async Task<IEnumerable<AssemblyRecord>> QueryAssemblyRecordsAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null)
  719. {
  720. var q = _db.Queryable<AssemblyRecord>();
  721. if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
  722. if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
  723. if (start.HasValue) q = q.Where(r => r.Timestamp >= start.Value);
  724. if (end.HasValue) q = q.Where(r => r.Timestamp < end.Value);
  725. var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
  726. return list;
  727. }
  728. /// <summary>
  729. /// 分页查询组装记录
  730. /// </summary>
  731. public async Task<(IEnumerable<AssemblyRecord> Items, int TotalCount)> QueryAssemblyRecordsPagedAsync(string recipeName = null, string productSN = null, DateTime? start = null, DateTime? end = null, int pageIndex = 1, int pageSize = 20)
  732. {
  733. if (pageIndex < 1) pageIndex = 1;
  734. if (pageSize < 1) pageSize = 20;
  735. var q = _db.Queryable<AssemblyRecord>();
  736. if (!string.IsNullOrEmpty(recipeName)) q = q.Where(r => r.RecipeName == recipeName);
  737. if (!string.IsNullOrEmpty(productSN)) q = q.Where(r => r.ProductSN == productSN);
  738. if (start.HasValue) q = q.Where(r => r.Timestamp >= start.Value);
  739. if (end.HasValue) q = q.Where(r => r.Timestamp < end.Value);
  740. var total = await q.CountAsync();
  741. var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
  742. return (list, total);
  743. }
  744. #endregion
  745. // Lock result methods
  746. /// <summary>
  747. /// 记录单次锁付结果(WaveDatas 不写入数据库)
  748. /// </summary>
  749. /// <param name="result"></param>
  750. public async Task RecordLockResultAsync(LockResult result)
  751. {
  752. try
  753. {
  754. if (result == null) throw new ArgumentNullException(nameof(result));
  755. if (result.Timestamp == default(DateTime)) result.Timestamp = DateTime.UtcNow;
  756. await _db.Insertable(result).ExecuteCommandAsync();
  757. }
  758. catch (Exception)
  759. {
  760. }
  761. }
  762. /// <summary>
  763. /// 分页查询锁付结果
  764. /// </summary>
  765. public async Task<(IEnumerable<LockResult> Items, int TotalCount)> QueryLockResultsAsync(DateTime start, DateTime end, int pageIndex, int pageSize, string productNumber = null, int? screwNumber = null, bool? lockPassed = null)
  766. {
  767. if (pageIndex < 1) pageIndex = 1;
  768. if (pageSize < 1) pageSize = 20;
  769. var q = _db.Queryable<LockResult>().Where(r => r.Timestamp >= start && r.Timestamp < end);
  770. if (!string.IsNullOrEmpty(productNumber)) q = q.Where(r => r.ProductNumber == productNumber);
  771. if (screwNumber.HasValue) q = q.Where(r => r.Number == screwNumber.Value);
  772. if (lockPassed.HasValue) q = q.Where(r => r.LockPassed == lockPassed.Value);
  773. var total = await q.CountAsync();
  774. var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToPageListAsync(pageIndex, pageSize);
  775. return (list, total);
  776. }
  777. /// <summary>
  778. /// 查询所有符合条件的锁付结果(不分页)。
  779. /// </summary>
  780. public async Task<IEnumerable<LockResult>> QueryAllLockResultsAsync(DateTime start, DateTime end, string productNumber = null, int? screwNumber = null, bool? lockPassed = null)
  781. {
  782. var q = _db.Queryable<LockResult>().Where(r => r.Timestamp >= start && r.Timestamp < end);
  783. if (!string.IsNullOrEmpty(productNumber)) q = q.Where(r => r.ProductNumber == productNumber);
  784. if (screwNumber.HasValue) q = q.Where(r => r.Number == screwNumber.Value);
  785. if (lockPassed.HasValue) q = q.Where(r => r.LockPassed == lockPassed.Value);
  786. var list = await q.OrderBy(r => r.Timestamp, OrderByType.Desc).ToListAsync();
  787. return list;
  788. }
  789. /// <summary>
  790. /// 记录一次螺丝供料器批次更换记录
  791. /// </summary>
  792. public async Task RecordScrewFeederBatchAsync(ScrewFeederBatchRecord record)
  793. {
  794. if (record == null) throw new ArgumentNullException(nameof(record));
  795. if (record.ChangeTime == default(DateTime)) record.ChangeTime = DateTime.UtcNow;
  796. await _db.Insertable(record).ExecuteCommandAsync();
  797. }
  798. #region 抛料
  799. /// <summary>
  800. /// 将一条抛料信息记录到数据库中
  801. /// </summary>
  802. /// <param name="record"></param>
  803. /// <returns></returns>
  804. /// <exception cref="ArgumentNullException"></exception>
  805. public async Task RecordThrowNumberAsync(ThrowRecord record)
  806. {
  807. if (record == null) throw new ArgumentNullException(nameof(record));
  808. record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
  809. await _db.Insertable(record).ExecuteCommandAsync();
  810. }
  811. /// <summary>
  812. /// 获取抛料数量
  813. /// </summary>
  814. /// <returns></returns>
  815. public async Task<int> GetThrowNumberAsync()
  816. {
  817. //今天的起始时间
  818. var todayStart = DateTime.Now.Date;
  819. //今天的结束时间
  820. var todayEnd = DateTime.Now.Date;
  821. todayEnd = todayEnd.AddDays(1);
  822. //按照时间范围查询抛料的总数
  823. var total = await _db.Queryable<ThrowRecord>()
  824. .Where(r => r.DayTime >= todayStart && r.DayTime < todayEnd)
  825. .SumAsync(r => r.ThrowCount);
  826. return total;
  827. }
  828. /// <summary>
  829. /// 重置抛料计数
  830. /// </summary>
  831. /// <returns></returns>
  832. public async Task ResetThrowNumberAsync()
  833. {
  834. //今天的起始时间
  835. var todayStart = DateTime.Now.Date;
  836. //今天的结束时间
  837. var todayEnd = DateTime.Now.Date;
  838. todayEnd = todayEnd.AddDays(1);
  839. await _db.Deleteable<ThrowRecord>().Where(r => r.DayTime >= todayStart && r.DayTime < todayEnd).ExecuteCommandAsync();
  840. }
  841. #endregion
  842. #region 产品和mes的绑定,自动切换产品
  843. /// <summary>
  844. /// 添加绑定信息
  845. /// </summary>
  846. /// <param name="record"></param>
  847. /// <param name="product"></param>
  848. /// <returns></returns>
  849. /// <exception cref="ArgumentNullException"></exception>
  850. public async Task InsertProWithMesAsync(ProductWithMes record, ProductModel product)
  851. {
  852. //record.Id = product.ID;
  853. record.ProductName = product.Name;
  854. await _db.Insertable(record).ExecuteCommandAsync();
  855. }
  856. /// <summary>
  857. /// 删除绑定信息
  858. /// </summary>
  859. /// <param name="record"></param>
  860. /// <param name="product"></param>
  861. /// <returns></returns>
  862. public async Task<int> DeleteProWithMesAsync(ProductModel product)
  863. {
  864. var v = await _db.Deleteable<ProductWithMes>().Where(r => r.ProductName == product.Name).ExecuteCommandAsync();
  865. return v;
  866. }
  867. /// <summary>
  868. /// 更新绑定信息
  869. /// </summary>
  870. /// <param name="record"></param>
  871. /// <param name="product"></param>
  872. /// <returns></returns>
  873. /// <exception cref="ArgumentNullException"></exception>
  874. public async Task<int> UpdateProductWithMesAsync(ProductWithMes record, ProductModel product)
  875. {
  876. if (record == null) throw new ArgumentNullException(nameof(record));
  877. var v = await _db.Updateable(record).Where(r => r.ProductName == product.Name).ExecuteCommandAsync();
  878. return v;
  879. }
  880. /// <summary>
  881. /// 查询绑定信息
  882. /// </summary>
  883. /// <param name="product"></param>
  884. /// <returns></returns>
  885. public async Task<ProductWithMes> QueryProductWithMesAsync(ProductModel product)
  886. {
  887. var total = await _db.Queryable<ProductWithMes>().Where(r => r.ProductName == product.Name).FirstAsync();
  888. return total;
  889. }
  890. /// <summary>
  891. /// 查询所有绑定信息
  892. /// </summary>
  893. /// <param name="product"></param>
  894. /// <returns></returns>
  895. public async Task<List<ProductWithMes>> QueryProductWithMesALLAsync()
  896. {
  897. var total = await _db.Queryable<ProductWithMes>().ToListAsync();
  898. return total;
  899. }
  900. /// <summary>
  901. /// 通过产品的编码和颜色查询产品
  902. /// </summary>
  903. /// <param name="code"></param>
  904. /// <returns></returns>
  905. public async Task<ProductWithMes> QueryProductFromMesAsync(string code, string color)
  906. {
  907. var v = await _db.Queryable<ProductWithMes>().Where(r => r.ProductCode == code && r.ProductColor == color).FirstAsync();
  908. return v;
  909. }
  910. /// <summary>
  911. /// 通过产品名称的查询产品
  912. /// </summary>
  913. /// <param name="code"></param>
  914. /// <returns></returns>
  915. public async Task<ProductWithMes> QueryProductFromMesAsync(string name)
  916. {
  917. var v = await _db.Queryable<ProductWithMes>().Where(r => r.ProductName == name).FirstAsync();
  918. return v;
  919. }
  920. #endregion
  921. }
  922. }