SystemDatabaseService.cs 40 KB

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