From 2cea1a791222d2a2df5afb1d7be34676d7cd2024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=92=9F=E5=96=9C=E6=97=BA?= Date: Mon, 29 Jul 2024 18:58:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BD=A8=E8=BF=B9=E9=A2=84=E8=AD=A6?= =?UTF-8?q?=E8=A7=84=E5=88=99=E7=BC=93=E5=AD=98=E4=BB=A5=E5=8F=8A=E9=9B=86?= =?UTF-8?q?=E7=BE=A4=E9=80=9A=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/SJZY.Track.Api/SjzyTrackApiModule.cs | 2 + .../Events/ModifyWarningEvent.cs | 23 +++ .../Services/IForewarningCacheService.cs | 12 ++ .../HostServices/RedisHostService.cs | 46 +++++ .../ForewarningCacheService.cs | 188 +++++++++++++++++- .../KpiAreaCacheService.cs | 21 +- .../ForewarningServices/KpiAreaService.cs | 16 +- .../IWarningRepository.cs | 7 + .../KPIAreaAggregate/IKpiAreaRepository.cs | 2 +- .../Consts/GlobalConst.Redis.cs | 15 ++ .../Dtos/Forewarning/KpiAreaInfo.cs | 25 +++ .../Dtos/Forewarning/WarningRuleCacheDto.cs | 20 ++ .../Repositories/KpiAreaRepository.cs | 14 +- .../Repositories/WarningRepository.cs | 26 +++ 14 files changed, 387 insertions(+), 30 deletions(-) create mode 100644 src/SJZY.Track.Application.Contract/Events/ModifyWarningEvent.cs create mode 100644 src/SJZY.Track.Application/HostServices/RedisHostService.cs create mode 100644 src/SJZY.Track.Domain/Dtos/Forewarning/WarningRuleCacheDto.cs diff --git a/src/SJZY.Track.Api/SjzyTrackApiModule.cs b/src/SJZY.Track.Api/SjzyTrackApiModule.cs index 13d9633a..a9cd95ae 100644 --- a/src/SJZY.Track.Api/SjzyTrackApiModule.cs +++ b/src/SJZY.Track.Api/SjzyTrackApiModule.cs @@ -9,6 +9,7 @@ using SJZY.Expand.ABP.Core.Prometheus; using SJZY.Expand.ABP.Core.Redis; using SJZY.Expand.ABP.Core.Swagger; using SJZY.Track.Application; +using SJZY.Track.Application.HostServices; using SJZY.Track.Repository; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; @@ -62,6 +63,7 @@ namespace SJZY.Track.Api Configure(options => { options.IsEnabled = false; }); context.Services.AddRedis(); + context.Services.AddHostedService(); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/src/SJZY.Track.Application.Contract/Events/ModifyWarningEvent.cs b/src/SJZY.Track.Application.Contract/Events/ModifyWarningEvent.cs new file mode 100644 index 00000000..265f00c2 --- /dev/null +++ b/src/SJZY.Track.Application.Contract/Events/ModifyWarningEvent.cs @@ -0,0 +1,23 @@ +namespace SJZY.Track.Application.Contract.Events; + +/// +/// redis 的发布订阅事件 +/// +public class ModifyWarningEvent:RedisEvent +{ + public string Key { get; set; } +} + +public class RedisEvent : IRedisEvent +{ + public string EventType { get; set; } +} + + +/// +/// 简单的事件接口,后续可以根据需要扩展 +/// +public interface IRedisEvent +{ + string EventType { get; set; } +} \ No newline at end of file diff --git a/src/SJZY.Track.Application.Contract/Services/IForewarningCacheService.cs b/src/SJZY.Track.Application.Contract/Services/IForewarningCacheService.cs index 8ee8c321..d7743c32 100644 --- a/src/SJZY.Track.Application.Contract/Services/IForewarningCacheService.cs +++ b/src/SJZY.Track.Application.Contract/Services/IForewarningCacheService.cs @@ -19,4 +19,16 @@ public interface IForewarningCacheService /// /// Task DeleteRulesAsync(int subSystemId, int customerId); + + /// + /// 重置本地缓存 + /// + /// + Task Clear(); + + /// + /// 清除redis 缓存 + /// + /// + Task ClearRedis(); } \ No newline at end of file diff --git a/src/SJZY.Track.Application/HostServices/RedisHostService.cs b/src/SJZY.Track.Application/HostServices/RedisHostService.cs new file mode 100644 index 00000000..702afb51 --- /dev/null +++ b/src/SJZY.Track.Application/HostServices/RedisHostService.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Hosting; +using SJZY.Expand.ABP.Core; +using SJZY.Track.Application.Contract.Events; +using SJZY.Track.Application.Contract.Services; +using SJZY.Track.Domain.Consts; + +namespace SJZY.Track.Application.HostServices; + +public class RedisHostService:IHostedService +{ + readonly IForewarningCacheService _forewarningCacheService; + + public RedisHostService(IForewarningCacheService forewarningCacheService) + { + _forewarningCacheService = forewarningCacheService; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // 订阅频道 + RedisHelper.Client.Subscribe(GetKey(), Subscribe); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + RedisHelper.Client.UnSubscribe(GetKey()); + return Task.CompletedTask; + } + + private void Subscribe(string message,object value) + { + if (value is string) + { + // 暂时只处理一个事件,不做反序列化 + //var instance = Newtonsoft.Json.JsonConvert.DeserializeObject(value as string); + } + + _forewarningCacheService.Clear(); + } + + private string GetKey() + { + return $"{GlobalConst.PublishNotifyChannelKey}"; + } +} \ No newline at end of file diff --git a/src/SJZY.Track.Application/Services/ForewarningServices/ForewarningCacheService.cs b/src/SJZY.Track.Application/Services/ForewarningServices/ForewarningCacheService.cs index fdd4463c..317e6c3a 100644 --- a/src/SJZY.Track.Application/Services/ForewarningServices/ForewarningCacheService.cs +++ b/src/SJZY.Track.Application/Services/ForewarningServices/ForewarningCacheService.cs @@ -1,29 +1,133 @@ +using FreeRedis; +using SJZY.Expand.ABP.Core; +using SJZY.Expand.ABP.Core.PublicMethords; using SJZY.Track.Application.Contract.Dtos.Forewarning; using SJZY.Track.Application.Contract.Services; +using SJZY.Track.Domain.Aggregates.ForewarningAggregate; +using SJZY.Track.Domain.Aggregates.KPIAreaAggregate; +using SJZY.Track.Domain.Consts; +using SJZY.Track.Domain.Dtos.Forewarning; using Volo.Abp.DependencyInjection; namespace SJZY.Track.Application.Services.ForewarningServices; public class ForewarningCacheService: IForewarningCacheService, ISingletonDependency { - private Dictionary> _rules = new(); - string AreaKey(int subSystemId) => $"TrackArea:{subSystemId}"; - string WarningKey(int subSystemId, int customerId) => $"TrackWarning:{subSystemId}:{customerId}"; + readonly IKpiAreaRepository _kpiAreaRepository; + readonly IWarningRepository _warningRepository; + const string GlobalKpiAreaLockKey = "KKpiAreaLockKey_GlobalLock"; + private const string GlobalWaningLockKey = "GlobalWaningLockKey_GlobalLock"; + + private Dictionary> _rules = new(); + private Dictionary>? _areas = new(); + readonly object _lock = new object(); + + public ForewarningCacheService(IKpiAreaRepository kpiAreaRepository, IWarningRepository warningRepository) + { + _kpiAreaRepository = kpiAreaRepository; + _warningRepository = warningRepository; + } + + + string WarningKey(int subSystemId, int customerId) => $"{GlobalConst.TrackWarningKeyPrefix}:{subSystemId}:{customerId}"; + + string WarningKey() => $"{GlobalConst.TrackWarningKeyPrefix}:all"; + + string AreaKey() + { + return $"{GlobalConst.TrackAreaKeyPrefix}"; + } + // 30w => 1w => local public async Task?> GetRulesAsync(int subSystemId, int customerId) { - var rules = _rules.GetValueOrDefault(WarningKey(subSystemId, customerId)); + var key = WarningKey(subSystemId, customerId); + var rules = _rules.GetValueOrDefault(key); if (rules == null) - { - // todo: 写入缓存 set str - await Task.Delay(1); + { + return await GetRules(subSystemId, customerId); } return rules; } + private List WriteRules(List rules) + { + List result = new List(); + foreach (var item in rules) + { + List? areaList = null; + if (item.AreaId.HasValue) + { + areaList = GetAreaInfoByAreaId(item.AreaId!.Value); + } + + var entry = new ForewarningRuleCacheEntry + { + Id = item.Id, + WarningId = item.WarningId, + TotalTimeSpan = item.TotalTimeSpan, + WarnTimeSpan = item.WarnTimeSpan, + WarnCount = item.WarnCount, + StartTrackStatusId = item.StartTrackStatusId, + EndTrackStatusId = item.EndTrackStatusId, + CargoType = item.CargoType, + Months = string.IsNullOrEmpty(item.Months)?new List():item.Months.Split('/').Select(int.Parse).ToList(), + IsTransit = item.IsTransit, + AreaId = areaList, + ProductCode = item.ProductCode, + LeavePort = item.LeavePort + }; + + result.Add(entry); + } + + return result; + } + + private async Task?> GetRules(int systemId, int customerId) + { + var key = WarningKey(); + var result = RedisHelper.Client.Get>(key); + if (result != null) + { + // 非线程安全,需要加锁 + lock (_lock) + { + _rules = result.GroupBy(val => WarningKey(val.SystemId, val.CustomerId)) + .ToDictionary(val => val.Key, val => WriteRules(val.ToList())); + + return _rules.GetValueOrDefault(WarningKey(systemId,customerId)); + } + } + + // 非线程安全,需要加锁 + using (RedisHelper.Client.Lock(GlobalWaningLockKey, 20)) + { + var ruleResult = _rules.GetValueOrDefault(WarningKey(systemId,customerId)); + if (ruleResult == null) + { + var warnings = await _warningRepository.GetAllWarningRule(); + if (warnings == null || warnings.Count == 0) + { + RedisHelper.Client.Set(key, new List(), TimeSpan.FromSeconds(20)); + return new List(); + } + + RedisHelper.Client.Set(key, warnings); + _rules = warnings.GroupBy(val => WarningKey(val.SystemId, val.CustomerId)) + .ToDictionary(val => val.Key, val => WriteRules(val.ToList())); + + return _rules.GetValueOrDefault(WarningKey(systemId,customerId)); + } + + return ruleResult; + } + + } + // a b c del local cusomer 3 del redis cache pub del redis cache public Task DeleteRulesAsync(int subSystemId, int customerId) { @@ -31,4 +135,74 @@ public class ForewarningCacheService: IForewarningCacheService, ISingletonDepend return Task.CompletedTask; } + + private List? GetAreaInfoByAreaId(int areaId) + { + if (_areas == null || _areas.Count == 0) + { + var areas = GetAreas(); + if(areas==null || areas.Count==0) return null; + _areas = areas.GroupBy(x => x.AreaId) + .ToDictionary(x => x.Key, x => x.Select(KpiAreaInfoToEntry).ToList()); + } + + return _areas.GetValueOrDefault(areaId); + } + + AreaCacheEntry KpiAreaInfoToEntry(KpiAreaInfo model) + { + return new AreaCacheEntry + { + Country = model.Country, + Province = model.Province, + City = model.City, + LeftPostcode = model.LeftPostcode, + RightPostcode = model.RightPostcode + }; + } + + /// + /// 获取所有区域 + /// + /// + private List? GetAreas() + { + var key = AreaKey(); + var result = RedisHelper.Client.Get>(key); + if (result == null) + { + using (RedisHelper.Client.Lock(GlobalKpiAreaLockKey, 20)) + { + result = RedisHelper.Client.Get>(key); + if (result != null) return new List(); + + var kpiArea = _kpiAreaRepository.GetAllAreaInfos(); + if (kpiArea == null) + { + RedisHelper.Client.Set(key, new List(), TimeSpan.FromSeconds(20)); + return new List(); + } + + RedisHelper.Client.Set(key, kpiArea); + return kpiArea; + } + } + + return result; + } + + public Task Clear() + { + _rules?.Clear(); + _areas?.Clear(); + return Task.CompletedTask; + } + + public Task ClearRedis() + { + RedisHelper.Client.Del(WarningKey()); + RedisHelper.Client.Del(AreaKey()); + return Task.CompletedTask; + } + } \ No newline at end of file diff --git a/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaCacheService.cs b/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaCacheService.cs index 65accbcf..4728dcf6 100644 --- a/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaCacheService.cs +++ b/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaCacheService.cs @@ -22,25 +22,8 @@ public class KpiAreaCacheService:IKpiAreaCacheService,ITransientDependency return $"{GlobalConst.KpiAreaKeyPrefix}:{systemId}:{country}"; } - public async Task GetKpiArea(int systemId, string country) + public Task GetKpiArea(int systemId, string country) { - var key = GetKey(systemId, country); - var result = RedisHelper.Client.Get(key); - if (result == null) - { - using (var globalLock = RedisHelper.Client.Lock(GlobalKpiAreaLockKey, 10)) - { - result = RedisHelper.Client.Get(key); - if (result != null) return; - - //todo 转换成KpiAreaInfo - var kpiArea =await _kpiAreaRepository.GetAllAreaInfos(); - if (kpiArea == null) - { - return; - } - RedisHelper.Client.Set(key, kpiArea, TimeSpan.FromSeconds(20)); - } - } + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaService.cs b/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaService.cs index 593467ef..a0057a41 100644 --- a/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaService.cs +++ b/src/SJZY.Track.Application/Services/ForewarningServices/KpiAreaService.cs @@ -1,4 +1,7 @@ +using SJZY.Expand.ABP.Core; +using SJZY.Track.Application.Contract.Services; using SJZY.Track.Domain.Aggregates.KPIAreaAggregate; +using SJZY.Track.Domain.Consts; using SJZY.Track.Domain.Dtos.Forewarning; namespace SJZY.Track.Application.Services.ForewarningServices; @@ -8,10 +11,12 @@ namespace SJZY.Track.Application.Services.ForewarningServices; public class KpiAreaService : ApplicationService { private readonly IKpiAreaRepository _ruleAreaRepository; + private readonly IForewarningCacheService _forewarningCacheService; - public KpiAreaService(IKpiAreaRepository ruleAreaRepository) + public KpiAreaService(IKpiAreaRepository ruleAreaRepository, IForewarningCacheService forewarningCacheService) { _ruleAreaRepository = ruleAreaRepository; + _forewarningCacheService = forewarningCacheService; } /// @@ -37,6 +42,8 @@ public class KpiAreaService : ApplicationService { var ruleAreaEntity = ObjectMapper.Map(input); var ruleInfoId = await _ruleAreaRepository.AddAreaInfoAsync(ruleAreaEntity); + await _forewarningCacheService.ClearRedis(); + await RedisHelper.Client.PublishAsync(GlobalConst.PublishNotifyChannelKey, "clear local cache"); return new UnifyResultDto(true, ruleInfoId, "添加成功"); } @@ -49,6 +56,8 @@ public class KpiAreaService : ApplicationService public async ValueTask DeleteKpiArea(int id) { await _ruleAreaRepository.DeleteArea(id); + await _forewarningCacheService.ClearRedis(); + await RedisHelper.Client.PublishAsync(GlobalConst.PublishNotifyChannelKey, "clear locl cache"); return new UnifyResultDto(true, "删除成功"); } @@ -61,6 +70,8 @@ public class KpiAreaService : ApplicationService public async ValueTask DeleteKpiAreaInfo(int id) { await _ruleAreaRepository.DeleteAreaInfo(id); + await _forewarningCacheService.ClearRedis(); + await RedisHelper.Client.PublishAsync(GlobalConst.PublishNotifyChannelKey, "clear local cache"); return new UnifyResultDto(true, "删除成功"); } @@ -101,6 +112,9 @@ public class KpiAreaService : ApplicationService var ruleAreaEntity = ObjectMapper.Map(input, areaInfo); await _ruleAreaRepository.UpdateAreaInfoAsync(ruleAreaEntity); + + await _forewarningCacheService.ClearRedis(); + await RedisHelper.Client.PublishAsync(GlobalConst.PublishNotifyChannelKey, "clear local cache"); return new UnifyResultDto(true, "更新成功"); } diff --git a/src/SJZY.Track.Domain/Aggregates/ForewarningAggregate/IWarningRepository.cs b/src/SJZY.Track.Domain/Aggregates/ForewarningAggregate/IWarningRepository.cs index efa73bbd..a864c05a 100644 --- a/src/SJZY.Track.Domain/Aggregates/ForewarningAggregate/IWarningRepository.cs +++ b/src/SJZY.Track.Domain/Aggregates/ForewarningAggregate/IWarningRepository.cs @@ -59,4 +59,11 @@ public interface IWarningRepository : IBasicRepository /// /// Task GetByIdAsync(int warningId); + + /// + /// 获取预警规则列表 + /// + /// + Task?> GetAllWarningRule(); + } \ No newline at end of file diff --git a/src/SJZY.Track.Domain/Aggregates/KPIAreaAggregate/IKpiAreaRepository.cs b/src/SJZY.Track.Domain/Aggregates/KPIAreaAggregate/IKpiAreaRepository.cs index c3c906d9..21d15342 100644 --- a/src/SJZY.Track.Domain/Aggregates/KPIAreaAggregate/IKpiAreaRepository.cs +++ b/src/SJZY.Track.Domain/Aggregates/KPIAreaAggregate/IKpiAreaRepository.cs @@ -64,5 +64,5 @@ public interface IKpiAreaRepository : IBasicRepository /// 获取所有区域信息 /// /// - Task> GetAllAreaInfos(); + List? GetAllAreaInfos(); } \ No newline at end of file diff --git a/src/SJZY.Track.Domain/Consts/GlobalConst.Redis.cs b/src/SJZY.Track.Domain/Consts/GlobalConst.Redis.cs index 376af72f..808d772a 100644 --- a/src/SJZY.Track.Domain/Consts/GlobalConst.Redis.cs +++ b/src/SJZY.Track.Domain/Consts/GlobalConst.Redis.cs @@ -42,4 +42,19 @@ public partial class GlobalConst /// 港口时区redis key前缀 /// public const string PortTimezoneKeyPrefix = "PortTimezone"; + + /// + /// 发布订阅通知频道Key + /// + public const string PublishNotifyChannelKey = "PublishNotifyChannel"; + + /// + /// 轨迹区域reids key + /// + public const string TrackAreaKeyPrefix = "TrackArea"; + + /// + /// 轨迹预警数据redis key前缀 + /// + public const string TrackWarningKeyPrefix = "TrackWarning"; } \ No newline at end of file diff --git a/src/SJZY.Track.Domain/Dtos/Forewarning/KpiAreaInfo.cs b/src/SJZY.Track.Domain/Dtos/Forewarning/KpiAreaInfo.cs index a9273783..eafc4d09 100644 --- a/src/SJZY.Track.Domain/Dtos/Forewarning/KpiAreaInfo.cs +++ b/src/SJZY.Track.Domain/Dtos/Forewarning/KpiAreaInfo.cs @@ -2,5 +2,30 @@ namespace SJZY.Track.Domain.Dtos.Forewarning; public class KpiAreaInfo { + public int AreaId { get; set; } + /// + /// 国家或地区 + /// + public string Country { get; set; } = null!; + + /// + /// 省份 + /// + public string? Province { get; set; } + + /// + /// 城市 + /// + public string? City { get; set; } + + /// + /// 开始邮编 + /// + public string? LeftPostcode { get; set; } + + /// + /// 截止邮编 + /// + public string? RightPostcode { get; set; } } \ No newline at end of file diff --git a/src/SJZY.Track.Domain/Dtos/Forewarning/WarningRuleCacheDto.cs b/src/SJZY.Track.Domain/Dtos/Forewarning/WarningRuleCacheDto.cs new file mode 100644 index 00000000..43b6b25c --- /dev/null +++ b/src/SJZY.Track.Domain/Dtos/Forewarning/WarningRuleCacheDto.cs @@ -0,0 +1,20 @@ +namespace SJZY.Track.Domain.Dtos.Forewarning; + +public class WarningRuleCacheDto +{ + public int Id { get; set; } + public int SystemId { get; set; } + public int CustomerId { get; set; } + public int WarningId { get; set; } + public float TotalTimeSpan { get; set; } + public float WarnTimeSpan { get; set; } + public int WarnCount { get; set; } + public int StartTrackStatusId { get; set; } + public int EndTrackStatusId { get; set; } + public string? CargoType { get; set; } + public string? Months { get; set; } + public int? AreaId { get; set; } + public bool? IsTransit { get; set; } + public string? LeavePort { get; set; } + public string? ProductCode { get; set; } +} \ No newline at end of file diff --git a/src/SJZY.Track.Repository/Repositories/KpiAreaRepository.cs b/src/SJZY.Track.Repository/Repositories/KpiAreaRepository.cs index f6226432..e2008c27 100644 --- a/src/SJZY.Track.Repository/Repositories/KpiAreaRepository.cs +++ b/src/SJZY.Track.Repository/Repositories/KpiAreaRepository.cs @@ -94,8 +94,18 @@ public class KpiAreaRepository : BasicRepository, IKpiAreaReposi /// 获取所有区域信息 /// /// - public Task> GetAllAreaInfos() + public List GetAllAreaInfos() { - return Context.Queryable().ToListAsync(); + return Context.Queryable() + .Select((x)=>new KpiAreaInfo + { + AreaId = x.AreaId, + Country = x.CountryCode, + LeftPostcode = x.LeftPostcode, + City = x.City, + Province = x.Province, + RightPostcode = x.RightPostcode + }) + .ToList(); } } \ No newline at end of file diff --git a/src/SJZY.Track.Repository/Repositories/WarningRepository.cs b/src/SJZY.Track.Repository/Repositories/WarningRepository.cs index fe25f1d7..23286a59 100644 --- a/src/SJZY.Track.Repository/Repositories/WarningRepository.cs +++ b/src/SJZY.Track.Repository/Repositories/WarningRepository.cs @@ -109,4 +109,30 @@ public class WarningRepository : BasicRepository, IWarningReposit return warning; } + + public Task?> GetAllWarningRule() + { + return Context.Queryable() + .InnerJoin((a, b) => a.WarningId == b.Id) + .Where((a,b)=>b.IsEnable) + .Select((a,b)=>new WarningRuleCacheDto + { + Id = a.Id, + WarningId = a.WarningId, + TotalTimeSpan = a.TotalTimeSpan, + WarnTimeSpan = a.WarnTimeSpan, + WarnCount = a.WarnCount, + StartTrackStatusId = a.StartTrackStatusId, + EndTrackStatusId = a.EndTrackStatusId, + CargoType = a.CargoType, + Months = a.Months, + AreaId = a.AreaId, + IsTransit = a.IsTransit, + LeavePort = a.LeavePort, + ProductCode = a.ProductCode, + CustomerId = b.CustomerId, + SystemId = b.SubSystemId + }) + .ToListAsync(); + } } \ No newline at end of file