SystemDatabaseService.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. /*
  2. 设计计划(伪代码) - 详尽步骤说明
  3. 目标:
  4. - 为 SystemDatabaseService 类添加完整的文档注释(XML 注释),
  5. 以便在 IDE 中显示方法契约和用途。
  6. - 保持现有实现逻辑不变,增加必要的参数检查(轻量级)以提高健壮性,
  7. 但不改变公开方法签名,确保兼容 ISystemDatabaseService。
  8. - 将伪代码通过文件顶部注释包含在文件内(满足“先描述计划,再给代码”的要求)。
  9. 伪代码步骤:
  10. 1. 在文件顶部插入多行注释,包含本次修改的目的和每个主要动作的摘要。
  11. 2. 为类 `SystemDatabaseService` 添加 XML 文档注释,说明职责和依赖。
  12. 3. 为每个公共方法添加 XML 注释(summary, param, returns)。
  13. - AddUserAsync(User user)
  14. - 验证 user 不为 null(如为 null 则抛出 ArgumentNullException)
  15. - 调用数据库插入并返回 user
  16. - DeleteUserAsync(int userId)
  17. - 直接调用 Deleteable 并返回是否删除到记录
  18. - GetAllUsersAsync()
  19. - 返回所有用户列表
  20. - GetUserByIdAsync(int userId)
  21. - 返回单一用户(InSingleAsync)
  22. - UpdateUserAsync(User user)
  23. - 验证 user 不为 null(如为 null 则抛出 ArgumentNullException)
  24. - 更新并返回 user
  25. - AuthenticateUserAsync(string username, string password)
  26. - 验证输入参数不为 null 或空(轻量级)
  27. - 使用 Queryable 查询并返回匹配用户(FirstAsync)
  28. - RecordProductionAsync(ProductionRecord record)
  29. - 验证 record 不为 null(如为 null 则抛出 ArgumentNullException)
  30. - 确保 record.Id 非空(Guid),插入记录
  31. - ResetProductionAsync(string productName = null)
  32. - 如果 productName 为空,清空表,否则按产品名删除
  33. - GetTotalProductionAsync(string productName, IEnumerable<string> categories = null)
  34. - 构造查询,按类别可选过滤,求和数量并返回
  35. - GetProductionStatsAsync(ProductionStatQuery query)
  36. - 验证 query 不为 null(如为 null 则抛出 ArgumentNullException)
  37. - 基于 query 参数构造查询(产品名、时间范围、类别)
  38. - 拉取列表后根据 GroupBy(hour/day/week/month/total)分组聚合
  39. - 返回聚合结果集合
  40. - 私有方法 FirstDayOfWeek(DateTime dt)
  41. - 计算给定日期的本周第一天(星期一),用于 week 分组
  42. 4. 确保所有新增的参数检查使用 .NET Framework 4.8 可用的异常类型。
  43. 5. 保持现有第三方 API(SqlSugar)调用不变,避免引入新依赖。
  44. 6. 生成的代码应能在 VS 中正确显示注释并通过编译(不改变方法签名)。
  45. 备注:
  46. - 不会尝试修改或新增接口方法签名(以确保与 ISystemDatabaseService 保持兼容)。
  47. - 本次变更侧重于文档注释与轻量的输入验证,避免对业务逻辑做重大调整。
  48. */
  49. using SqlSugar;
  50. using System;
  51. using System.Collections.Generic;
  52. using System.Linq;
  53. using System.Threading.Tasks;
  54. using TeamAAS_VP.Interfaces;
  55. using TeamAAS_VP.Models;
  56. namespace TeamAAS_VP.Data
  57. {
  58. /// <summary>
  59. /// 系统数据库服务,基于 <see cref="ISqlSugarClient"/> 提供对应用数据的访问。
  60. /// 包含用户增删改查、用户身份验证以及生产记录的写入与统计功能。
  61. /// </summary>
  62. public class SystemDatabaseService : ISystemDatabaseService
  63. {
  64. private readonly ISqlSugarClient _db;
  65. private User CurrentUser { get; set; }
  66. /// <summary>
  67. /// 使用指定的 SqlSugar 客户端构造数据库服务实例。
  68. /// </summary>
  69. /// <param name="db">已配置的 <see cref="ISqlSugarClient"/> 实例。</param>
  70. public SystemDatabaseService(ISqlSugarClient db)
  71. {
  72. _db = db;
  73. }
  74. #region 用户
  75. // User CRUD
  76. /// <summary>
  77. /// 添加新用户到数据库。
  78. /// 对于 SqlSugar,User.Id 为数据库自增值,因此在此方法中不应设定 Id。
  79. /// </summary>
  80. /// <param name="user">要添加的用户对象,不能为空。</param>
  81. /// <returns>返回传入的用户对象(Id 由数据库生成后会回写到该对象)。</returns>
  82. /// <exception cref="ArgumentNullException">当 <paramref name="user"/> 为 null 时抛出。</exception>
  83. public async Task<User> AddUserAsync(User user)
  84. {
  85. if (user == null) throw new ArgumentNullException(nameof(user));
  86. // for SqlSugar User.Id is int identity, so do not set it here
  87. await _db.Insertable(user).ExecuteCommandAsync();
  88. return user;
  89. }
  90. /// <summary>
  91. /// 根据用户 Id 删除用户。
  92. /// </summary>
  93. /// <param name="userId">要删除的用户 Id。</param>
  94. /// <returns>如果删除了至少一条记录则返回 true,否则返回 false。</returns>
  95. public async Task<bool> DeleteUserAsync(int userId)
  96. {
  97. var r = await _db.Deleteable<User>().In(userId).ExecuteCommandAsync();
  98. return r > 0;
  99. }
  100. /// <summary>
  101. /// 获取所有用户。
  102. /// </summary>
  103. /// <returns>用户列表的异步枚举。</returns>
  104. public async Task<IEnumerable<User>> GetAllUsersAsync()
  105. {
  106. return await _db.Queryable<User>().ToListAsync();
  107. }
  108. /// <summary>
  109. /// 根据用户 Id 查询单个用户。
  110. /// </summary>
  111. /// <param name="userId">用户 Id。</param>
  112. /// <returns>匹配的用户,如果未找到则返回 null(取决于 SqlSugar 的行为)。</returns>
  113. public async Task<User> GetUserByIdAsync(int userId)
  114. {
  115. return await _db.Queryable<User>().InSingleAsync(userId);
  116. }
  117. /// <summary>
  118. /// 更新用户信息并返回更新后的对象。
  119. /// </summary>
  120. /// <param name="user">要更新的用户对象,不能为空。</param>
  121. /// <returns>返回传入的用户对象。</returns>
  122. /// <exception cref="ArgumentNullException">当 <paramref name="user"/> 为 null 时抛出。</exception>
  123. public async Task<User> UpdateUserAsync(User user)
  124. {
  125. if (user == null) throw new ArgumentNullException(nameof(user));
  126. var r = await _db.Updateable(user).ExecuteCommandAsync();
  127. return user;
  128. }
  129. // User authentication
  130. /// <summary>
  131. /// 验证用户名和密码并返回匹配的用户。
  132. /// 注意:当前实现直接以明文密码比较,请根据需要在调用侧或更高层添加哈希/盐策略。
  133. /// </summary>
  134. /// <param name="username">用户名,不能为空或空字符串。</param>
  135. /// <param name="password">密码,不能为空或空字符串。</param>
  136. /// <returns>匹配的用户对象,如果未找到则可能抛出或返回 null(取决于 SqlSugar 的 FirstAsync 行为)。</returns>
  137. /// <exception cref="ArgumentException">当 <paramref name="username"/> 或 <paramref name="password"/> 为空时抛出。</exception>
  138. public async Task<User> AuthenticateUserAsync(string username, string password)
  139. {
  140. if (string.IsNullOrEmpty(username)) throw new ArgumentException("username is required", nameof(username));
  141. if (string.IsNullOrEmpty(password)) throw new ArgumentException("password is required", nameof(password));
  142. return await _db.Queryable<User>().Where(u => u.UserName == username && u.UserPassword == password).FirstAsync();
  143. }
  144. // User login / current user helpers
  145. /// <summary>
  146. /// 将指定用户设为当前用户(写入一条成功的登录记录)。
  147. /// </summary>
  148. /// <param name="userId">要设为当前用户的用户 Id。</param>
  149. public async Task SetCurrentUserAsync(int userId)
  150. {
  151. // 验证用户存在
  152. var user = await GetUserByIdAsync(userId);
  153. if (user == null)
  154. {
  155. throw new ArgumentException($"User with id {userId} not found", nameof(userId));
  156. }
  157. CurrentUser = user;
  158. // 记录一条成功的登录记录
  159. var rec = new UserLoginRecord
  160. {
  161. UserId = userId,
  162. Success = true,
  163. Time = DateTime.UtcNow,
  164. Message = "Set as current user"
  165. };
  166. await RecordUserLoginAsync(rec);
  167. }
  168. /// <summary>
  169. /// 获取最近一次成功登录的用户(视为当前用户)。
  170. /// </summary>
  171. /// <returns>若存在成功登录记录则返回对应用户,否则返回 null。</returns>
  172. public Task<User> GetCurrentUserAsync()
  173. {
  174. return Task.FromResult(GetCurrentUser());
  175. }
  176. /// <summary>
  177. /// 获取当前用户
  178. /// </summary>
  179. /// <returns></returns>
  180. public User GetCurrentUser()
  181. {
  182. //var recs = _db.Queryable<UserLoginRecord>()
  183. // .Where(r => r.Success == true)
  184. // .OrderBy(r => r.Time, OrderByType.Desc)
  185. // .Take(1)
  186. // .ToList();
  187. //var rec = recs.FirstOrDefault();
  188. ////如果没有登录记录则返回一个默认的用户,即第一个用户
  189. //if (rec == null) return _db.Queryable<User>().First();
  190. //return _db.Queryable<User>().InSingle(rec.UserId);
  191. if (CurrentUser==null)
  192. {
  193. CurrentUser= _db.Queryable<User>().ToList().FirstOrDefault();
  194. }
  195. return CurrentUser;
  196. }
  197. /// <summary>
  198. /// 记录一条用户登录记录(成功或失败)。
  199. /// </summary>
  200. /// <param name="record">登录记录对象,不能为空。</param>
  201. public async Task RecordUserLoginAsync(UserLoginRecord record)
  202. {
  203. if (record == null) throw new ArgumentNullException(nameof(record));
  204. if (record.Time == default(DateTime)) record.Time = DateTime.UtcNow;
  205. await _db.Insertable(record).ExecuteCommandAsync();
  206. }
  207. /// <summary>
  208. /// 获取指定用户最近的一条登录记录(无论是否成功)。
  209. /// </summary>
  210. /// <param name="userId">用户 Id。</param>
  211. /// <returns>最近的一条登录记录或 null。</returns>
  212. public async Task<UserLoginRecord> GetLastUserLoginRecordAsync(int userId)
  213. {
  214. var recs = await _db.Queryable<UserLoginRecord>()
  215. .Where(r => r.UserId == userId)
  216. .OrderBy(r => r.Time, OrderByType.Desc)
  217. .Take(1)
  218. .ToListAsync();
  219. return recs.FirstOrDefault();
  220. }
  221. /// <summary>
  222. /// 将指定用户名标记为“记住”,并取消其他用户的记住标志。
  223. /// </summary>
  224. /// <param name="username">要记住的用户名。</param>
  225. public async Task SetRememberUserAsync(string username)
  226. {
  227. if (string.IsNullOrEmpty(username)) throw new ArgumentException("username is required", nameof(username));
  228. // 使用事务先清除所有记住标志,再为指定用户设置记住
  229. try
  230. {
  231. _db.Ado.BeginTran();
  232. // 清除所有
  233. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = false }).Where("1=1").ExecuteCommandAsync();
  234. // 设置指定用户为记住
  235. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = true }).Where(u => u.UserName == username).ExecuteCommandAsync();
  236. _db.Ado.CommitTran();
  237. }
  238. catch
  239. {
  240. try { _db.Ado.RollbackTran(); } catch { }
  241. throw;
  242. }
  243. }
  244. /// <summary>
  245. /// 清除所有用户的记住标志。
  246. /// </summary>
  247. public async Task ClearRememberUserAsync()
  248. {
  249. await _db.Updateable<User>().SetColumns(u => new User { IsRemember = false }).ExecuteCommandAsync();
  250. }
  251. /// <summary>
  252. /// 获取当前被标记为记住的用户(如果存在)。
  253. /// </summary>
  254. /// <returns>被记住的用户或 null。</returns>
  255. public async Task<User> GetRememberedUserAsync()
  256. {
  257. return await _db.Queryable<User>().Where(u => u.IsRemember == true).FirstAsync();
  258. }
  259. #endregion
  260. #region 生产记录
  261. // Production records
  262. /// <summary>
  263. /// 记录一次生产事件。如果 <see cref="ProductionRecord.Id"/> 为 <see cref="Guid.Empty"/>,
  264. /// 则会自动生成新的 Guid。
  265. /// </summary>
  266. /// <param name="record">生产记录对象,不能为空。</param>
  267. /// <returns>异步任务。</returns>
  268. /// <exception cref="ArgumentNullException">当 <paramref name="record"/> 为 null 时抛出。</exception>
  269. public async Task RecordProductionAsync(ProductionRecord record)
  270. {
  271. if (record == null) throw new ArgumentNullException(nameof(record));
  272. // 如果未指定产生该记录的用户名,尝试使用当前登录用户
  273. if (string.IsNullOrEmpty(record.UserName))
  274. {
  275. try
  276. {
  277. var current = await GetCurrentUserAsync();
  278. if (current != null)
  279. {
  280. record.UserName = current.UserName;
  281. }
  282. }
  283. catch
  284. {
  285. // 忽略获取当前用户时的错误,继续保存记录(保守策略)
  286. }
  287. }
  288. record.Id = record.Id == Guid.Empty ? Guid.NewGuid() : record.Id;
  289. await _db.Insertable(record).ExecuteCommandAsync();
  290. }
  291. /// <summary>
  292. /// 重置生产记录。若未传入 <paramref name="productName"/>,则清空所有生产记录;
  293. /// 否则仅删除指定产品名的记录。
  294. /// </summary>
  295. /// <param name="productName">可选的产品名过滤。</param>
  296. /// <returns>异步任务。</returns>
  297. public async Task ResetProductionAsync(string productName = null)
  298. {
  299. if (string.IsNullOrEmpty(productName))
  300. {
  301. await _db.Deleteable<ProductionRecord>().Where("1=1").ExecuteCommandAsync();
  302. }
  303. else
  304. {
  305. await _db.Deleteable<ProductionRecord>().Where(r => r.ProductName == productName).ExecuteCommandAsync();
  306. }
  307. }
  308. /// <summary>
  309. /// 获取指定产品(和可选类别)在数据库中累计的产量总和(仅统计 Quantity &gt; 0 的记录)。
  310. /// </summary>
  311. /// <param name="productName">产品名,不能为空或空字符串。</param>
  312. /// <param name="categories">可选的类别集合,用于过滤。</param>
  313. /// <returns>指定条件下的产量总和(整数)。</returns>
  314. /// <exception cref="ArgumentException">当 <paramref name="productName"/> 为空时抛出。</exception>
  315. public async Task<int> GetTotalProductionAsync(string productName, IEnumerable<string> categories = null)
  316. {
  317. if (string.IsNullOrEmpty(productName)) throw new ArgumentException("productName is required", nameof(productName));
  318. var q = _db.Queryable<ProductionRecord>().Where(r => r.ProductName == productName && r.Quantity > 0);
  319. if (categories != null && categories.Any())
  320. {
  321. q = q.Where(r => categories.Contains(r.Category));
  322. }
  323. var sum = await q.SumAsync<int>(r => r.Quantity);
  324. return sum;
  325. }
  326. /// <summary>
  327. /// 获取生产统计结果,按指定时间范围、产品名和可选类别筛选,
  328. /// 并根据 <see cref="ProductionStatQuery.GroupBy"/>(hour/day/week/month/total)进行分组聚合。
  329. /// </summary>
  330. /// <param name="query">生产统计查询对象,不能为空,必须包含 ProductName/Start/End 等。</param>
  331. /// <returns>按指定分组粒度返回的统计结果集合。</returns>
  332. /// <exception cref="ArgumentNullException">当 <paramref name="query"/> 为 null 时抛出。</exception>
  333. public async Task<IEnumerable<ProductionStatResult>> GetProductionStatsAsync(ProductionStatQuery query)
  334. {
  335. if (query == null) throw new ArgumentNullException(nameof(query));
  336. if (string.IsNullOrEmpty(query.ProductName)) throw new ArgumentException("ProductName is required in query", nameof(query));
  337. var q = _db.Queryable<ProductionRecord>().Where(r => r.ProductName == query.ProductName && r.Timestamp >= query.Start && r.Timestamp < query.End);
  338. if (query.Categories != null && query.Categories.Any())
  339. {
  340. q = q.Where(r => query.Categories.Contains(r.Category));
  341. }
  342. var list = await q.ToListAsync();
  343. var results = new List<ProductionStatResult>();
  344. switch ((query.GroupBy ?? "total").ToLower())
  345. {
  346. case "hour":
  347. // group by hour
  348. var hours = list.GroupBy(r => new DateTime(r.Timestamp.Year, r.Timestamp.Month, r.Timestamp.Day, r.Timestamp.Hour, 0, 0))
  349. .OrderBy(g => g.Key);
  350. foreach (var g in hours)
  351. {
  352. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddHours(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  353. }
  354. break;
  355. case "day":
  356. var days = list.GroupBy(r => r.Timestamp.Date).OrderBy(g => g.Key);
  357. foreach (var g in days)
  358. {
  359. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddDays(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  360. }
  361. break;
  362. case "week":
  363. // ISO week grouping by week start (Monday)
  364. var weeks = list.GroupBy(r => FirstDayOfWeek(r.Timestamp)).OrderBy(g => g.Key);
  365. foreach (var g in weeks)
  366. {
  367. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddDays(7), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  368. }
  369. break;
  370. case "month":
  371. var months = list.GroupBy(r => new DateTime(r.Timestamp.Year, r.Timestamp.Month, 1)).OrderBy(g => g.Key);
  372. foreach (var g in months)
  373. {
  374. results.Add(new ProductionStatResult { PeriodStart = g.Key, PeriodEnd = g.Key.AddMonths(1), Quantity = g.Sum(x => x.Quantity), ProductName = query.ProductName });
  375. }
  376. break;
  377. default:
  378. results.Add(new ProductionStatResult { PeriodStart = query.Start, PeriodEnd = query.End, Quantity = list.Sum(x => x.Quantity), ProductName = query.ProductName });
  379. break;
  380. }
  381. return results;
  382. }
  383. /// <summary>
  384. /// 获取所有已知分类相加起来的今天的产能
  385. /// </summary>
  386. /// <param name="productName"></param>
  387. /// <returns></returns>
  388. public async Task<int> GetTodayProductionAsync(string productName)
  389. {
  390. //今天的起始时间
  391. var todayStart = DateTime.UtcNow.Date;
  392. //今天的结束时间
  393. var todayEnd = DateTime.UtcNow.Date;
  394. todayEnd= todayEnd.AddDays(1);
  395. //按照产品名称和时间范围查询生产记录的总数
  396. var total = await _db.Queryable<ProductionRecord>()
  397. .Where(r => r.ProductName == productName && r.Timestamp >= todayStart && r.Timestamp < todayEnd)
  398. .SumAsync(r => r.Quantity);
  399. return total;
  400. }
  401. /// <summary>
  402. /// 获取所有已知分类相加起来的当前时间往前的一个小时的产能
  403. /// </summary>
  404. /// <param name="productName"></param>
  405. /// <returns></returns>
  406. public async Task<int> GetLastHourProductionAsync(string productName)
  407. {
  408. var lastHourStart = DateTime.UtcNow.AddHours(-1);
  409. var lastHourEnd = DateTime.UtcNow;
  410. //按照产品名称和时间范围查询生产记录的总数
  411. var total = await _db.Queryable<ProductionRecord>()
  412. .Where(r => r.ProductName == productName && r.Timestamp >= lastHourStart && r.Timestamp < lastHourEnd)
  413. .SumAsync(r => r.Quantity);
  414. return total;
  415. }
  416. /// <summary>
  417. /// 获取当前月的产能
  418. /// </summary>
  419. /// <param name="productName"></param>
  420. /// <returns></returns>
  421. public Task<int> GetCurrentMonthProductionAsync(string productName)
  422. {
  423. var now = DateTime.UtcNow;
  424. var monthStart = new DateTime(now.Year, now.Month, 1);
  425. var monthEnd = monthStart.AddMonths(1);
  426. //按照产品名称和时间范围查询生产记录的总数
  427. return _db.Queryable<ProductionRecord>()
  428. .Where(r => r.ProductName == productName && r.Timestamp >= monthStart && r.Timestamp < monthEnd)
  429. .SumAsync(r => r.Quantity);
  430. }
  431. /// <summary>
  432. /// 获取当前周的产能
  433. /// </summary>
  434. /// <param name="productName"></param>
  435. /// <returns></returns>
  436. public Task<int> GetCurrentWeekProductionAsync(string productName)
  437. {
  438. var now = DateTime.UtcNow;
  439. var weekStart = FirstDayOfWeek(now);
  440. var weekEnd = weekStart.AddDays(7);
  441. //按照产品名称和时间范围查询生产记录的总数
  442. return _db.Queryable<ProductionRecord>()
  443. .Where(r => r.ProductName == productName && r.Timestamp >= weekStart && r.Timestamp < weekEnd)
  444. .SumAsync(r => r.Quantity);
  445. }
  446. /// <summary>
  447. /// 计算给定日期所属周的第一天(以星期一为一周的第一天)。
  448. /// 返回结果为日期的 Date 部分(时间为 00:00:00)。
  449. /// </summary>
  450. /// <param name="dt">输入日期。</param>
  451. /// <returns>该日期对应周的周一日期(Date 部分)。</returns>
  452. private DateTime FirstDayOfWeek(DateTime dt)
  453. {
  454. var diff = (7 + (dt.DayOfWeek - DayOfWeek.Monday)) % 7;
  455. return dt.Date.AddDays(-1 * diff);
  456. }
  457. /// <summary>
  458. /// 获取指定产品名称的总产能
  459. /// </summary>
  460. /// <param name="productName"></param>
  461. /// <returns></returns>
  462. public Task<int> GetOverallProductionAsync(string productName)
  463. {
  464. return _db.Queryable<ProductionRecord>()
  465. .Where(r => r.ProductName == productName)
  466. .SumAsync(r => r.Quantity);
  467. }
  468. /// <summary>
  469. /// 获取指定产品的所有已知分类。
  470. /// </summary>
  471. /// <param name="productName">要查询分类的产品名称。</param>
  472. /// <returns>
  473. /// 一个字符串集合,包含该产品的所有分类名称;如果没有分类,返回空集合或 null(建议返回空集合)。
  474. /// </returns>
  475. public async Task<IEnumerable<string>> GetProductCategoriesAsync(string productName)
  476. {
  477. var categories = await _db.Queryable<ProductionRecord>()
  478. .Where(r => r.ProductName == productName)
  479. .Select(r => r.Category)
  480. .Distinct()
  481. .ToListAsync();
  482. return categories;
  483. }
  484. /// <summary>
  485. /// 获取指定产品名称的所有记录按照分类的产能统计
  486. /// </summary>
  487. /// <param name="productName"></param>
  488. /// <returns></returns>
  489. public async Task<Dictionary<string, int>> GetProductionByCategoriesAsync(string productName)
  490. {
  491. var list = await _db.Queryable<ProductionRecord>()
  492. .Where(r => r.ProductName == productName)
  493. .ToListAsync();
  494. var dict = list.GroupBy(r => r.Category)
  495. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  496. return dict;
  497. }
  498. /// <summary>
  499. /// 获取指定产品名称的一个月内按照每天统计对应的类别和数量
  500. /// </summary>
  501. /// <param name="productName"></param>
  502. /// <returns></returns>
  503. public async Task<Dictionary<DateTime, Dictionary<string, int>>> GetMonthlyProductionByDayAndCategoryAsync(string productName)
  504. {
  505. var now = DateTime.UtcNow;
  506. var monthStart = new DateTime(now.Year, now.Month, 1);
  507. var monthEnd = monthStart.AddMonths(1);
  508. var list = await _db.Queryable<ProductionRecord>()
  509. .Where(r => r.ProductName == productName && r.Timestamp >= monthStart && r.Timestamp < monthEnd)
  510. .ToListAsync();
  511. var dict = new Dictionary<DateTime, Dictionary<string, int>>();
  512. var days = list.GroupBy(r => r.Timestamp.Date).OrderBy(g => g.Key);
  513. foreach (var dayGroup in days)
  514. {
  515. var categoryDict = dayGroup.GroupBy(r => r.Category)
  516. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  517. dict[dayGroup.Key] = categoryDict;
  518. }
  519. return dict;
  520. }
  521. /// <summary>
  522. /// 获取指定产品名称的一个星期内按照每天统计对应的类别和数量
  523. /// </summary>
  524. /// <param name="productName"></param>
  525. /// <returns></returns>
  526. public async Task<Dictionary<int, Dictionary<string, int>>> GetWeeklyProductionByDayAndCategoryAsync(string productName)
  527. {
  528. var now = DateTime.UtcNow;
  529. var weekStart = FirstDayOfWeek(now);
  530. var weekEnd = weekStart.AddDays(7);
  531. var list = await _db.Queryable<ProductionRecord>()
  532. .Where(r => r.ProductName == productName && r.Timestamp >= weekStart && r.Timestamp < weekEnd)
  533. .ToListAsync();
  534. var dict = new Dictionary<int, Dictionary<string, int>>();
  535. var days = list.GroupBy(r => r.Timestamp.DayOfWeek).OrderBy(g => g.Key);
  536. foreach (var dayGroup in days)
  537. {
  538. var categoryDict = dayGroup.GroupBy(r => r.Category)
  539. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  540. dict[(int)dayGroup.Key] = categoryDict;
  541. }
  542. return dict;
  543. }
  544. /// <summary>
  545. /// 获取指定产品名称的当天内按照每小时统计对应的类别和数量
  546. /// </summary>
  547. /// <param name="productName"></param>
  548. /// <returns></returns>
  549. public async Task<Dictionary<int, Dictionary<string, int>>> GetDailyProductionByHourAndCategoryAsync(string productName)
  550. {
  551. var now = DateTime.UtcNow;
  552. var dayStart = now.Date;
  553. var dayEnd = dayStart.AddDays(1);
  554. var list = await _db.Queryable<ProductionRecord>()
  555. .Where(r => r.ProductName == productName && r.Timestamp >= dayStart && r.Timestamp < dayEnd)
  556. .ToListAsync();
  557. var dict = new Dictionary<int, Dictionary<string, int>>();
  558. var hours = list.GroupBy(r => r.Timestamp.Hour).OrderBy(g => g.Key);
  559. foreach (var hourGroup in hours)
  560. {
  561. var categoryDict = hourGroup.GroupBy(r => r.Category)
  562. .ToDictionary(g => g.Key, g => g.Sum(r => r.Quantity));
  563. dict[hourGroup.Key] = categoryDict;
  564. }
  565. return dict;
  566. }
  567. #endregion
  568. #region 报警记录
  569. // Alarm Records
  570. /// <summary>
  571. /// 记录一条报警记录。
  572. /// </summary>
  573. /// <param name="alarm">报警记录对象,不能为空。</param>
  574. public async Task RecordAlarmAsync(AlarmRecord alarm)
  575. {
  576. if (alarm == null) throw new ArgumentNullException(nameof(alarm));
  577. if (alarm.Time == default(DateTime)) alarm.Time = DateTime.UtcNow;
  578. await _db.Insertable(alarm).ExecuteCommandAsync();
  579. }
  580. /// <summary>
  581. /// 按时间范围和可选条件查询报警记录。
  582. /// </summary>
  583. /// <param name="start">起始时间(含)。</param>
  584. /// <param name="end">结束时间(不含)。</param>
  585. /// <param name="code">可选的报警编号过滤。</param>
  586. /// <param name="source">可选的报警来源过滤。</param>
  587. /// <returns>满足条件的报警记录集合。</returns>
  588. public async Task<IEnumerable<AlarmRecord>> QueryAlarmsAsync(DateTime start, DateTime end, string code = null, string source = null)
  589. {
  590. var q = _db.Queryable<AlarmRecord>().Where(a => a.Time >= start && a.Time < end);
  591. if (!string.IsNullOrEmpty(code)) q = q.Where(a => a.Code == code);
  592. if (!string.IsNullOrEmpty(source)) q = q.Where(a => a.Source == source);
  593. return await q.OrderBy(a => a.Time, OrderByType.Desc).ToListAsync();
  594. }
  595. #endregion
  596. }
  597. }