WarningService.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. using EasyTemplate.Tool;
  2. using EasyTemplate.Tool.Entity;
  3. using EasyTemplate.Tool.Entity.App;
  4. using SqlSugar;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Linq;
  8. using System.Threading.Tasks;
  9. namespace EasyTemplate.Service
  10. {
  11. /// <summary>
  12. /// 预警告统计服务
  13. /// </summary>
  14. public class WarningService
  15. {
  16. private readonly SqlSugarRepository<TPrewarning> _prewarning;
  17. private readonly SqlSugarRepository<TRecord> _record;
  18. /// <summary>
  19. /// 累计状态快照(用于缓存中间计算结果)
  20. /// </summary>
  21. private class AccumulatedState
  22. {
  23. public int AccOver { get; set; } // 累计超标记录
  24. public int AccOverAlert { get; set; } // 累计严重超标记录
  25. public int AccTotal { get; set; } // 累计总数
  26. public int AccOver2 { get; set; } // 累计超标2记录
  27. public int ContinueDays { get; set; } // 连续超标天数
  28. public int ContinueDays2 { get; set; } // 连续超标2天数
  29. public int ContinueDaysAlert { get; set; } // 连续严重超标天数
  30. public int LastRate { get; set; } // 最后累计超标率
  31. public int LastRate2 { get; set; } // 最后超标率2
  32. public int LastRateAlert { get; set; } // 最后严重超标率
  33. }
  34. public WarningService(SqlSugarRepository<TPrewarning> prewarning, SqlSugarRepository<TRecord> record)
  35. {
  36. _prewarning = prewarning;
  37. _record = record;
  38. }
  39. /// <summary>
  40. /// 获取 35 天内所有油枪的统计数据
  41. /// </summary>
  42. public async Task<List<WarningStatistics>> Get35DaysStatisticsAsync()
  43. {
  44. var endDate = DateOnly.FromDateTime(DateTime.Today);
  45. var startDate = endDate.AddDays(-34); // 包含今天共 35 天
  46. return await CalculateStatisticsAsync(startDate, endDate);
  47. }
  48. /// <summary>
  49. /// 按日期范围获取统计数据
  50. /// </summary>
  51. public async Task<List<WarningStatistics>> GetStatisticsByDateRangeAsync(DateOnly startDate, DateOnly endDate)
  52. {
  53. return await CalculateStatisticsAsync(startDate, endDate);
  54. }
  55. /// <summary>
  56. /// 按油枪和日期范围筛选统计数据
  57. /// </summary>
  58. public async Task<List<WarningStatistics>> GetStatisticsByFilterAsync(WarningStatisticsQuery query)
  59. {
  60. var endDate = query.EndDate ?? DateOnly.FromDateTime(DateTime.Today);
  61. var startDate = query.StartDate ?? endDate.AddDays(-34);
  62. var allStats = await CalculateStatisticsAsync(startDate, endDate);
  63. // 按油枪筛选
  64. if (query.NozzleId.HasValue)
  65. {
  66. allStats = allStats.Where(s => s.nozzle == query.NozzleId.Value).ToList();
  67. }
  68. return allStats;
  69. }
  70. /// <summary>
  71. /// 更新指定日期的统计数据(用于新交易插入后)
  72. /// </summary>
  73. public async Task UpdateDailyStatisticsAsync(DateOnly date, int nozzleId)
  74. {
  75. try
  76. {
  77. Console.WriteLine($"[预警告统计] 开始更新油枪{nozzleId},日期:{date} 的统计数据");
  78. // 重新计算该油枪在该日期的统计
  79. var stats = await CalculateSingleDayStatisticsAsync(date, nozzleId);
  80. if (stats != null)
  81. {
  82. Console.WriteLine($"[预警告统计] 更新成功 - 总笔数:{stats.daily_total}, 超标:{stats.daily_overproof}, 超标率:{stats.daily_overproofrate}%");
  83. }
  84. }
  85. catch (Exception ex)
  86. {
  87. Console.WriteLine($"[预警告统计] 更新失败:{ex.Message}");
  88. }
  89. }
  90. /// <summary>
  91. /// 计算单个日期的统计数据
  92. /// </summary>
  93. private async Task<WarningStatistics> CalculateSingleDayStatisticsAsync(DateOnly date, int nozzleId)
  94. {
  95. // 获取当天的预警告记录
  96. var prewarning = await _prewarning.AsQueryable()
  97. .Where(p => p.date == date && p.nozzle == nozzleId)
  98. .FirstAsync();
  99. if (prewarning == null)
  100. {
  101. return null;
  102. }
  103. return new WarningStatistics
  104. {
  105. nozzle = nozzleId,
  106. statistics_date = date,
  107. daily_total = prewarning.total,
  108. daily_overproof = prewarning.overproof,
  109. daily_overproofrate = prewarning.overproofrate,
  110. daily_overproof_alert = prewarning.overproof_alert,
  111. daily_overproofrate_alert = prewarning.overproofrate_alert,
  112. daily_overproof_2 = prewarning.overproof_2,
  113. daily_overproofrate_2 = prewarning.overproofrate_2,
  114. is_accumulated = prewarning.total >= 5,
  115. accumulated_days = 1
  116. };
  117. }
  118. /// <summary>
  119. /// 核心统计计算逻辑(优化版:使用缓存避免重复计算)
  120. /// </summary>
  121. private async Task<List<WarningStatistics>> CalculateStatisticsAsync(DateOnly startDate, DateOnly endDate)
  122. {
  123. Console.WriteLine($"[预警告统计] 开始计算统计,范围:{startDate} 至 {endDate}");
  124. var results = new List<WarningStatistics>();
  125. // 获取该日期范围内的所有预警告记录
  126. var prewarnings = await _prewarning.AsQueryable()
  127. .Where(p => p.date >= startDate && p.date <= endDate)
  128. .ToListAsync();
  129. // 按油枪分组
  130. var groupedByNozzle = prewarnings.GroupBy(p => p.nozzle);
  131. foreach (var nozzleGroup in groupedByNozzle)
  132. {
  133. var nozzleId = nozzleGroup.Key;
  134. var sortedData = nozzleGroup.OrderBy(p => p.date).ToList();
  135. // 创建状态缓存数组,存储每一天的累计状态快照
  136. var stateCache = new AccumulatedState[sortedData.Count];
  137. // 为每一天计算统计数据
  138. for (int i = 0; i < sortedData.Count; i++)
  139. {
  140. var currentPrewarning = sortedData[i];
  141. var currentDate = currentPrewarning.date;
  142. var stats = new WarningStatistics
  143. {
  144. nozzle = nozzleId,
  145. statistics_date = currentDate,
  146. daily_total = currentPrewarning.total,
  147. daily_overproof = currentPrewarning.overproof,
  148. daily_overproofrate = currentPrewarning.overproofrate,
  149. daily_overproof_alert = currentPrewarning.overproof_alert,
  150. daily_overproofrate_alert = currentPrewarning.overproofrate_alert,
  151. daily_overproof_2 = currentPrewarning.overproof_2,
  152. daily_overproofrate_2 = currentPrewarning.overproofrate_2
  153. };
  154. // 计算 35 天累计统计(使用缓存优化)
  155. CalculateAccumulatedStatisticsWithCache(sortedData, i, stats, stateCache);
  156. results.Add(stats);
  157. }
  158. }
  159. Console.WriteLine($"[预警告统计] 计算完成,共{results.Count}条记录");
  160. return results;
  161. }
  162. /// <summary>
  163. /// 计算累计统计数据(优化版:使用状态缓存)
  164. /// 规则:从最早日期正向迭代到当前日期,累加统计并在满足条件时清零
  165. /// 使用前一天的缓存结果继续计算,避免重复计算
  166. /// </summary>
  167. private void CalculateAccumulatedStatisticsWithCache(List<TPrewarning> sortedData, int currentIndex, WarningStatistics stats, AccumulatedState[] stateCache)
  168. {
  169. // 获取区域规则配置
  170. bool isBeijingMode = VRRules.RefReachCount;
  171. int refCount = VRRules.RefCount;
  172. int refPrewarningIndex = VRRules.RefPrewarningIndex;
  173. int refPrewarningIndexAlert = VRRules.RefPrewarningIndexAlert;
  174. bool isZhejiangSpecial = VRRules.BCountAllDay;
  175. // 如果是第一天,从头开始计算
  176. if (currentIndex == 0)
  177. {
  178. var day = sortedData[0];
  179. var newState = new AccumulatedState();
  180. // 累加当天数据
  181. newState.AccOver = day.overproof;
  182. newState.AccOverAlert = day.overproof_alert;
  183. newState.AccTotal = day.total;
  184. newState.AccOver2 = day.overproof_2;
  185. // 计算超标率
  186. newState.LastRate = newState.AccTotal == 0 ? 0 : (newState.AccOver * 100) / newState.AccTotal;
  187. newState.LastRate2 = newState.AccTotal == 0 ? 0 : (newState.AccOver2 * 100) / newState.AccTotal;
  188. newState.LastRateAlert = newState.AccTotal == 0 ? 0 : (newState.AccOverAlert * 100) / newState.AccTotal;
  189. // 设置统计结果
  190. SetStatsFromState(stats, newState);
  191. // 根据模式判断并更新连续天数
  192. ApplyWarningRules(newState, isBeijingMode, refCount, refPrewarningIndex,
  193. refPrewarningIndexAlert, isZhejiangSpecial);
  194. // 保存到缓存
  195. stateCache[0] = newState;
  196. SetStatsFromState_continue_days(stats, newState);
  197. return;
  198. }
  199. // 非第一天:复制前一天的状态
  200. var previousState = stateCache[currentIndex - 1];
  201. var currentState = new AccumulatedState
  202. {
  203. AccOver = previousState.AccOver,
  204. AccOverAlert = previousState.AccOverAlert,
  205. AccTotal = previousState.AccTotal,
  206. AccOver2 = previousState.AccOver2,
  207. ContinueDays = previousState.ContinueDays,
  208. ContinueDays2 = previousState.ContinueDays2,
  209. ContinueDaysAlert = previousState.ContinueDaysAlert
  210. };
  211. // 累加当天数据
  212. var currentDay = sortedData[currentIndex];
  213. currentState.AccOver += currentDay.overproof;
  214. currentState.AccOverAlert += currentDay.overproof_alert;
  215. currentState.AccTotal += currentDay.total;
  216. currentState.AccOver2 += currentDay.overproof_2;
  217. // 计算超标率
  218. currentState.LastRate = currentState.AccTotal == 0 ? 0 : (currentState.AccOver * 100) / currentState.AccTotal;
  219. currentState.LastRate2 = currentState.AccTotal == 0 ? 0 : (currentState.AccOver2 * 100) / currentState.AccTotal;
  220. currentState.LastRateAlert = currentState.AccTotal == 0 ? 0 : (currentState.AccOverAlert * 100) / currentState.AccTotal;
  221. // 设置统计结果
  222. SetStatsFromState(stats, currentState);
  223. // 根据模式判断并更新连续天数
  224. ApplyWarningRules(currentState, isBeijingMode, refCount, refPrewarningIndex,
  225. refPrewarningIndexAlert, isZhejiangSpecial);
  226. // 保存到缓存
  227. stateCache[currentIndex] = currentState;
  228. SetStatsFromState_continue_days(stats, currentState);
  229. }
  230. /// <summary>
  231. /// 应用预警告规则(北京模式/标准模式)
  232. /// </summary>
  233. private void ApplyWarningRules(AccumulatedState state, bool isBeijingMode, int refCount,
  234. int refPrewarningIndex, int refPrewarningIndexAlert, bool isZhejiangSpecial)
  235. {
  236. if (isBeijingMode)//isBeijingMode
  237. {
  238. // === 北京模式(ref_reach_count)===
  239. // 规则:超标率>=阈值 且 总笔数>=5 时,连续天数 +1
  240. // 普通超标判断
  241. if (state.LastRate >= refPrewarningIndex)
  242. {
  243. if (state.AccTotal >= 5)
  244. {
  245. state.ContinueDays++;
  246. }
  247. }
  248. else
  249. {
  250. if (state.AccTotal >= 5)
  251. {
  252. state.ContinueDays = 0;
  253. }
  254. }
  255. // 严重超标判断
  256. if (state.LastRateAlert >= refPrewarningIndexAlert)
  257. {
  258. if (state.AccTotal >= 5)
  259. {
  260. state.ContinueDaysAlert++;
  261. }
  262. }
  263. else
  264. {
  265. if (state.AccTotal >= 5)
  266. {
  267. state.ContinueDaysAlert = 0;
  268. }
  269. }
  270. // 已经算入一天,清零累加器
  271. state.AccOver = 0;
  272. state.AccOverAlert = 0;
  273. state.AccTotal = 0;
  274. state.AccOver2 = 0;
  275. }
  276. else
  277. {
  278. // === 标准模式 ===
  279. // 规则:总笔数>=ref_count(通常 5) 时才检查超标率
  280. if (state.AccTotal >= refCount)
  281. {
  282. // 普通超标判断
  283. if (state.LastRate >= refPrewarningIndex)
  284. {
  285. state.ContinueDays++;
  286. }
  287. else
  288. {
  289. state.ContinueDays = 0;
  290. }
  291. // 严重超标判断
  292. if (state.LastRateAlert >= refPrewarningIndexAlert)
  293. {
  294. state.ContinueDaysAlert++;
  295. }
  296. else
  297. {
  298. state.ContinueDaysAlert = 0;
  299. }
  300. // 超标 2 判断(广东中石化)
  301. if (state.LastRate2 >= refPrewarningIndex)
  302. {
  303. state.ContinueDays2++;
  304. }
  305. else
  306. {
  307. state.ContinueDays2 = 0;
  308. }
  309. // 已经算入一天,清零累加器
  310. state.AccOver = 0;
  311. state.AccOverAlert = 0;
  312. state.AccTotal = 0;
  313. state.AccOver2 = 0;
  314. }
  315. else
  316. {
  317. // 浙江中石化特殊逻辑
  318. if (isZhejiangSpecial)
  319. {
  320. if (state.ContinueDays > 0)
  321. {
  322. state.ContinueDays++;
  323. }
  324. if (state.ContinueDaysAlert > 0)
  325. {
  326. state.ContinueDaysAlert++;
  327. }
  328. }
  329. }
  330. }
  331. }
  332. /// <summary>
  333. /// 从状态对象设置统计结果
  334. /// </summary>
  335. private void SetStatsFromState(WarningStatistics stats, AccumulatedState state)
  336. {
  337. stats.continue_days = state.ContinueDays;
  338. stats.continue_days_2 = state.ContinueDays2;
  339. stats.continue_days_alert = state.ContinueDaysAlert;
  340. stats.last_overproofrate = state.LastRate;
  341. stats.last_overproofrate_2 = state.LastRate2;
  342. stats.last_overproofrate_alert = state.LastRateAlert;
  343. // 同时保留原有的累计字段(用于兼容)
  344. stats.total_count = state.AccTotal;
  345. stats.total_overproof = state.AccOver;
  346. stats.total_overproof_alert = state.AccOverAlert;
  347. stats.total_overproof_2 = state.AccOver2;
  348. stats.total_overproofrate = state.LastRate;
  349. stats.total_overproofrate_2 = state.LastRate2;
  350. stats.total_overproofrate_alert = state.LastRateAlert;
  351. }
  352. private void SetStatsFromState_continue_days(WarningStatistics stats, AccumulatedState state)
  353. {
  354. stats.continue_days = state.ContinueDays;
  355. stats.continue_days_2 = state.ContinueDays2;
  356. stats.continue_days_alert = state.ContinueDaysAlert;
  357. }
  358. /// <summary>
  359. /// 计算累计统计数据(原始版本,保留用于兼容)
  360. /// 规则:从最早日期正向迭代到当前日期,累加统计并在满足条件时清零
  361. /// </summary>
  362. private void CalculateAccumulatedStatistics(List<TPrewarning> sortedData, int currentIndex, WarningStatistics stats)
  363. {
  364. // 累加器初始化(对应 C++ acc_over, acc_over_alert, acc_total, acc_over_2)
  365. int acc_over = 0; // 累计超标记录
  366. int acc_over_alert = 0; // 累计严重超标记录
  367. int acc_total = 0; // 累计总数
  368. int acc_over_2 = 0; // 累计超标 2 记录(广东中石化)
  369. // 连续天数计数器
  370. int continue_days = 0;
  371. int continue_days_2 = 0;
  372. int continue_days_alert = 0;
  373. // 最后计算的超标率
  374. int last_rate = 0;
  375. int last_rate_2 = 0;
  376. int last_rate_alert = 0;
  377. // 获取区域规则配置
  378. bool isBeijingMode = VRRules.RefReachCount; // 北京模式:ref_reach_count
  379. int refCount = VRRules.RefCount; // 标准模式阈值(通常 5)
  380. int refPrewarningIndex = VRRules.RefPrewarningIndex; // 超标率阈值
  381. int refPrewarningIndexAlert = VRRules.RefPrewarningIndexAlert; // 严重超标率阈值
  382. bool isZhejiangSpecial = VRRules.BCountAllDay; // 浙江中石化特殊逻辑
  383. // 正向迭代:从第 0 天到当前天(对应 C++ mapDayWarning.begin() 到 end)
  384. for (int i = 0; i <= currentIndex && i < sortedData.Count; i++)
  385. {
  386. var day = sortedData[i];
  387. // 累加数据
  388. acc_over += day.overproof;
  389. acc_over_alert += day.overproof_alert;
  390. acc_total += day.total;
  391. acc_over_2 += day.overproof_2;
  392. // 计算累计超标率(避免除以 0)
  393. last_rate = acc_total == 0 ? 0 : (acc_over * 100) / acc_total;
  394. last_rate_2 = acc_total == 0 ? 0 : (acc_over_2 * 100) / acc_total;
  395. last_rate_alert = acc_total == 0 ? 0 : (acc_over_alert * 100) / acc_total;
  396. // 更新警告对象中的累计值(用于显示)
  397. // 注意:这里不直接设置 stats,因为可能还需要清零
  398. if (isBeijingMode)
  399. {
  400. // === 北京模式(ref_reach_count)===
  401. // 规则:超标率>=阈值 且 总笔数>=5 时,连续天数 +1
  402. // 普通超标判断
  403. if (last_rate >= refPrewarningIndex)
  404. {
  405. if (acc_total >= 5)
  406. {
  407. continue_days++;
  408. }
  409. }
  410. else
  411. {
  412. if (acc_total >= 5)
  413. {
  414. continue_days = 0;
  415. }
  416. }
  417. // 严重超标判断
  418. if (last_rate_alert >= refPrewarningIndexAlert)
  419. {
  420. if (acc_total >= 5)
  421. {
  422. continue_days_alert++;
  423. }
  424. }
  425. else
  426. {
  427. if (acc_total >= 5)
  428. {
  429. continue_days_alert = 0;
  430. }
  431. }
  432. // 已经算入一天,清零累加器
  433. acc_over = 0;
  434. acc_over_alert = 0;
  435. acc_total = 0;
  436. acc_over_2 = 0;
  437. }
  438. else
  439. {
  440. // === 标准模式 ===
  441. // 规则:总笔数>=ref_count(通常 5) 时才检查超标率
  442. if (acc_total >= refCount)
  443. {
  444. // 普通超标判断
  445. if (last_rate >= refPrewarningIndex)
  446. {
  447. continue_days++;
  448. }
  449. else
  450. {
  451. continue_days = 0;
  452. }
  453. // 严重超标判断
  454. if (last_rate_alert >= refPrewarningIndexAlert)
  455. {
  456. continue_days_alert++;
  457. }
  458. else
  459. {
  460. continue_days_alert = 0;
  461. }
  462. // 超标 2 判断(广东中石化)
  463. if (last_rate_2 >= refPrewarningIndex) // || day.continueoverproof == 1
  464. {
  465. continue_days_2++;
  466. }
  467. else
  468. {
  469. continue_days_2 = 0;
  470. }
  471. // 已经算入一天,清零累加器
  472. acc_over = 0;
  473. acc_over_alert = 0;
  474. acc_total = 0;
  475. acc_over_2 = 0;
  476. }
  477. else
  478. {
  479. // 该分支是 ref_count 大于 0(即需要进行累计)的情况下,
  480. // 而 acc_total 小于 ref_count 时进入
  481. if (isZhejiangSpecial)
  482. {
  483. // === 浙江中石化特殊逻辑 ===
  484. // 不足 5 笔时延续之前的预警状态
  485. if (continue_days > 0)
  486. {
  487. continue_days++;
  488. }
  489. if (continue_days_alert > 0)
  490. {
  491. continue_days_alert++;
  492. }
  493. }
  494. }
  495. }
  496. }
  497. // 设置最终统计结果
  498. stats.continue_days = continue_days;
  499. stats.continue_days_2 = continue_days_2;
  500. stats.continue_days_alert = continue_days_alert;
  501. stats.last_overproofrate = last_rate;
  502. stats.last_overproofrate_2 = last_rate_2;
  503. stats.last_overproofrate_alert = last_rate_alert;
  504. // 同时保留原有的累计字段(用于兼容)
  505. stats.total_count = acc_total;
  506. stats.total_overproof = acc_over;
  507. stats.total_overproof_alert = acc_over_alert;
  508. stats.total_overproof_2 = acc_over_2;
  509. stats.total_overproofrate = last_rate;
  510. stats.total_overproofrate_2 = last_rate_2;
  511. stats.total_overproofrate_alert = last_rate_alert;
  512. }
  513. /// <summary>
  514. /// 获取所有有数据的油枪 ID 列表
  515. /// </summary>
  516. public async Task<List<int>> GetAllNozzleIdsAsync()
  517. {
  518. var nozzles = await _prewarning.AsQueryable()
  519. .Select(p => p.nozzle)
  520. .Distinct()
  521. .ToListAsync();
  522. return nozzles;
  523. }
  524. /// <summary>
  525. /// 获取统计日期范围(最早到最新)
  526. /// </summary>
  527. public async Task<(DateOnly minDate, DateOnly maxDate)> GetDateRangeAsync()
  528. {
  529. var dates = await _prewarning.AsQueryable()
  530. .Select(p => p.date)
  531. .ToListAsync();
  532. if (dates == null || dates.Count == 0)
  533. {
  534. return (new DateOnly(), new DateOnly());
  535. }
  536. var minDate = dates.Min();
  537. var maxDate = dates.Max();
  538. return (minDate, maxDate);
  539. }
  540. }
  541. }