diff --git a/SiteManagementSystem(SoftwareEngineering)/Extension/IServiceCollectionExtension.cs b/SiteManagementSystem(SoftwareEngineering)/Extension/IServiceCollectionExtension.cs index 634b761..1414540 100644 --- a/SiteManagementSystem(SoftwareEngineering)/Extension/IServiceCollectionExtension.cs +++ b/SiteManagementSystem(SoftwareEngineering)/Extension/IServiceCollectionExtension.cs @@ -1,6 +1,7 @@ using SiteManagementSystem_SoftwareEngineering_.Configuration; using SiteManagementSystem_SoftwareEngineering_.Factory; using SiteManagementSystem_SoftwareEngineering_.Interface; +using SiteManagementSystem_SoftwareEngineering_.Model; using SiteManagementSystem_SoftwareEngineering_.Service; using static SiteManagementSystem_SoftwareEngineering_.Service.UserManagerService; @@ -24,18 +25,38 @@ namespace SiteManagementSystem_SoftwareEngineering_.Extension services.AddScoped(_ => new TokenFactory(config)); return services; } - public static IServiceCollection AddUserManager(this IServiceCollection services, Action options) + + public static IServiceCollection AddUserManager( + this IServiceCollection services, + Action options + ) { var config = new SecretConfig(); options(config); - if (config.HashSalt is null) throw new ArgumentNullException(nameof(config.HashSalt) + "can not be null"); + if (config.HashSalt is null) + throw new ArgumentNullException(nameof(config.HashSalt) + "can not be null"); services.AddScoped(services => { var logger = services.GetRequiredService>(); - var storageService= services.GetRequiredService(); + var storageService = services.GetRequiredService(); return new UserManageService(storageService, logger, config.HashSalt); }); return services; } + + public static IServiceCollection AddRedisUtils( + this IServiceCollection services, + Action options + ) + { + var config = new RedisConfig(); + options(config); + if (config.RedisConnectionString is null) + throw new ArgumentNullException("Redis ConnectionString is null."); + services.AddSingleton(services => + new RedisUtils(config.RedisConnectionString, config.RedisDB, services.GetRequiredService>()) + ); + return services; + } } } diff --git a/SiteManagementSystem(SoftwareEngineering)/Interface/ICacheService.cs b/SiteManagementSystem(SoftwareEngineering)/Interface/ICacheService.cs new file mode 100644 index 0000000..3a75d9c --- /dev/null +++ b/SiteManagementSystem(SoftwareEngineering)/Interface/ICacheService.cs @@ -0,0 +1,12 @@ +namespace SiteManagementSystem_SoftwareEngineering_.Interface +{ + public interface ICacheService + { + public Action? ExpiredCallbackFunc { set; } + public int Capacity { get; set; } + public Task SetAsync(string key, string value, TimeSpan? expiry = null); + public Task GetAsync(string key); + public Task RemoveAsync(string key); + public Task RemoveLatestKeyAsync(); + } +} diff --git a/SiteManagementSystem(SoftwareEngineering)/Model/RedisConfig.cs b/SiteManagementSystem(SoftwareEngineering)/Model/RedisConfig.cs new file mode 100644 index 0000000..f7d65c0 --- /dev/null +++ b/SiteManagementSystem(SoftwareEngineering)/Model/RedisConfig.cs @@ -0,0 +1,8 @@ +namespace SiteManagementSystem_SoftwareEngineering_.Model +{ + public class RedisConfig + { + public int RedisDB { get; set; } + public string RedisConnectionString { get; set; } = null!; + } +} diff --git a/SiteManagementSystem(SoftwareEngineering)/Program.cs b/SiteManagementSystem(SoftwareEngineering)/Program.cs index 92c8358..5b0eb12 100644 --- a/SiteManagementSystem(SoftwareEngineering)/Program.cs +++ b/SiteManagementSystem(SoftwareEngineering)/Program.cs @@ -2,6 +2,7 @@ using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using SiteManagementSystem_SoftwareEngineering_.Extension; using SiteManagementSystem_SoftwareEngineering_.Interface; @@ -53,7 +54,13 @@ builder }) .AddUserManager(options => options.HashSalt = builder.Configuration.GetValue("SecretSalt")! - ).AddScoped(); + ).AddScoped() + .AddRedisUtils(options => + { + options.RedisConnectionString = builder.Configuration.GetConnectionString("Redis")!; + options.RedisDB = int.Parse(builder.Configuration["RedisDB"] ?? "0"); + } + ).AddSingleton(typeof(ICacheService<>), typeof(RedisService<>)); var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/SiteManagementSystem(SoftwareEngineering)/Service/FieldService.cs b/SiteManagementSystem(SoftwareEngineering)/Service/FieldService.cs index b38ec37..5c5e969 100644 --- a/SiteManagementSystem(SoftwareEngineering)/Service/FieldService.cs +++ b/SiteManagementSystem(SoftwareEngineering)/Service/FieldService.cs @@ -22,9 +22,9 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service private readonly ScriptEngine _engine = Python.CreateEngine(); private readonly dynamic _service; - public FieldService(SQLService storageService) + public FieldService(SQLService storageService,ICacheServicecacheService) { - _layer = new EntityFrameworkPythonCompatibilityAndInterpretationLayer(storageService); + _layer = new EntityFrameworkPythonCompatibilityAndInterpretationLayer(storageService,cacheService); var _engine = Python.CreateEngine(); var searchPaths = _engine.GetSearchPaths(); searchPaths.Add(@"./Service/PythonServiceFile/"); @@ -36,6 +36,11 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service _service = scope.GetVariable("Service")(); } + public void RedisKeyExpired(string key) + { + var keys = key.Split('$', 2); + _service.RedisKeyExpired(keys[0], keys[1]); + } public void AddField(string name, string value) { var t = (Field) @@ -65,14 +70,26 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service { public readonly SQLHelperService FieldDb; public readonly SQLHelperService RecordDb; + public readonly SQLHelperService FinishedFieldRecordDb; + private readonly ICacheService _cacheService; - public EntityFrameworkPythonCompatibilityAndInterpretationLayer(SQLService storageService) + public EntityFrameworkPythonCompatibilityAndInterpretationLayer(SQLService storageService, ICacheService cacheService) { + _cacheService = cacheService; FieldDb = new SQLHelperService(storageService, nameof(storageService.Fields)); RecordDb = new SQLHelperService( storageService, nameof(storageService.UserFieldRecords) ); + FinishedFieldRecordDb = new SQLHelperService(storageService,nameof(storageService.FinishedFieldRecords)); + } + public void SetTimeOut(DateTime endTime,string info) + { + var now = DateTime.Now; + if (endTime <= now) + throw new InvalidOperationException("The end must be later than now."); + var span = endTime - now; + _cacheService.SetAsync(endTime.ToString() + "$" + info, "", span); } } diff --git a/SiteManagementSystem(SoftwareEngineering)/Service/PythonServiceFile/layer.py b/SiteManagementSystem(SoftwareEngineering)/Service/PythonServiceFile/layer.py index 904ec8e..cf9ed64 100644 --- a/SiteManagementSystem(SoftwareEngineering)/Service/PythonServiceFile/layer.py +++ b/SiteManagementSystem(SoftwareEngineering)/Service/PythonServiceFile/layer.py @@ -1,4 +1,4 @@ -import clr +import clr from user import User from field import Field from user_field import FieldRecord @@ -11,6 +11,7 @@ class Layer: def __init__(self, layer): self._field_db = layer.FieldDb self._record_db = layer.RecordDb + self._finished_field_recoed_db = layer.FinishedFieldRecordDb def add_field(self, field: Field) -> 'Layer': self._field_db.Add(field.parse_to_csharp_object(self._field_db.GetDefaultEntity())) @@ -35,3 +36,16 @@ class Layer: def remove_record(self, record: FieldRecord) -> 'Layer': self._record_db.TryDelete("Uid", record.parse_to_csharp_object(self._record_db.GetDefaultEntity()).Uid) return self + + def add_finished_record(self, record: UserFieldRecord) -> 'Layer': + self._finished_field_recoed_db.Add(record.parse_to_csharp_object(self._finished_field_recoed_db.GetDefaultEntity())) + return self + + def find_finished_record(self, key: str, value: str) -> [UserFieldRecord]: + return [UserFieldRecord(t) for t in self._finished_field_recoed_db.FindAll(key, Guid.Parse(value))] + + # One User only allowed to book one field at a time + def remove_finished_record(self, record: UserFieldRecord) -> 'Layer': + self._finished_field_recoed_db.TryDelete("Uid", record.parse_to_csharp_object(self._finished_field_recoed_db.GetDefaultEntity()).Uid) + return self + diff --git a/SiteManagementSystem(SoftwareEngineering)/Service/RedisService.cs b/SiteManagementSystem(SoftwareEngineering)/Service/RedisService.cs new file mode 100644 index 0000000..779a75a --- /dev/null +++ b/SiteManagementSystem(SoftwareEngineering)/Service/RedisService.cs @@ -0,0 +1,116 @@ +using SiteManagementSystem_SoftwareEngineering_.Interface; +using StackExchange.Redis; +using System.Configuration; + +namespace SiteManagementSystem_SoftwareEngineering_.Service +{ + public class RedisService(ILogger> logger, RedisUtils redisHelper) : ICacheService + { + private readonly ILogger> _logger = logger; + private readonly RedisUtils _redisHelper = redisHelper; + private readonly LinkedList _values = []; + private readonly string _categoryName = typeof(TCategoryName).Name; + public int Capacity { get; set; } = int.MaxValue; + private Action? _expiredCallbackFunc = null; + public Action? ExpiredCallbackFunc + { + set + { + _expiredCallbackFunc = value; + _redisHelper.SetExpiredCallbackFunction(_categoryName, KeyDrop); + } + } + + public async Task SetAsync(string key, string value, TimeSpan? expiry = null) + { + // 现在默认容量是最大值,除非调用方设置容量,否则不会调用 RemoveLatestKeyAsync() + while (_values.Count >= Capacity) + await RemoveLatestKeyAsync(); + _values.AddLast(key); + return await _redisHelper.SetAsync(_categoryName, key, value, expiry); + } + + public async Task GetAsync(string key) => + await _redisHelper.GetAsync(_categoryName, key); + + public async Task RemoveAsync(string key) => + await _redisHelper.RemoveAsync(_categoryName, key); + + /// + /// 删除现有的最早加入的数据 + /// + /// + public async Task RemoveLatestKeyAsync() + { + if (_values.Count == 0) + return false; + var key = _values.First(); + _logger.LogWarning($"{key} dropped due to limited capacity."); + KeyDrop(key); + return await RemoveAsync(key); + } + + private void KeyDrop(string key) + { + _values.Remove(key); + _expiredCallbackFunc?.Invoke(key); + } + } + + // Redis: CONFIG set notify-keyspace-events Ex + // Key style: typeName+ ' ' + key + // IDistributedCache 确实封装了一些有用的方法 但是其最大的问题是无法在键过期后主动发出事件 + // 内部的RedisCache类没有保留IConnectionMultiplexer 而是获取了 dataBase 后就舍弃了 + // 因此 继承再反射不是一个很好的选项,不如重新封装 + public class RedisUtils + { + private readonly ConnectionMultiplexer _redis; + private readonly ILogger _logger; + private readonly Dictionary?> _expiredCallbackFunctions = []; + private readonly IDatabase _database; + private readonly int _numberOfDB; + + public RedisUtils(string connectionString,int redisDB, ILogger logger) + { + _numberOfDB = redisDB; + _logger = logger; + _redis = ConnectionMultiplexer.Connect(connectionString); + _redis + .GetSubscriber() + .Subscribe( + RedisChannel.Pattern($"__keyevent@{_numberOfDB}__:expired"), + (_, message) => + { + DataExpiredHandler(message); + } + ); + _database = _redis.GetDatabase(_numberOfDB); + // TODO: create a func to check if Redis setting is correct + } + + // typeName+ ' ' + key + // key is Hash, value is type.When key expired, you can't get value! + public async Task SetAsync( + string typeName, + string key, + string value, + TimeSpan? expiry = null + ) => await _database.StringSetAsync(typeName + " " + key, value, expiry); + + public async Task GetAsync(string typeName, string key) => + await _database.StringGetAsync(typeName + " " + key); + + public async Task RemoveAsync(string typeName, string key) => + await _database.KeyDeleteAsync(typeName + " " + key); + + public void SetExpiredCallbackFunction(string typeName, Action? func) => + _expiredCallbackFunctions[typeName] = func; + + private void DataExpiredHandler(RedisValue val) + { + var temp = val.ToString().Split(' '); + if (_expiredCallbackFunctions.TryGetValue(temp[0], out var func)) + func?.Invoke(temp[1]); + } + } +} diff --git a/SiteManagementSystem(SoftwareEngineering)/Service/SQLService.cs b/SiteManagementSystem(SoftwareEngineering)/Service/SQLService.cs index a47a781..a0c71db 100644 --- a/SiteManagementSystem(SoftwareEngineering)/Service/SQLService.cs +++ b/SiteManagementSystem(SoftwareEngineering)/Service/SQLService.cs @@ -1,5 +1,4 @@ - -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using SiteManagementSystem_SoftwareEngineering_.Entity; namespace SiteManagementSystem_SoftwareEngineering_.Service @@ -11,8 +10,10 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service { Database.EnsureCreated(); } + public DbSet Users { get; set; } public DbSet Fields { get; set; } - public DbSet UserFieldRecords{get;set;} + public DbSet UserFieldRecords { get; set; } + public DbSet FinishedFieldRecords { get; set; } } } diff --git a/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj b/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj index 568cff5..012639d 100644 --- a/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj +++ b/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj @@ -14,6 +14,7 @@ + diff --git a/SiteManagementSystem(SoftwareEngineering)/appsettings.json b/SiteManagementSystem(SoftwareEngineering)/appsettings.json index 324e0ec..19222d0 100644 --- a/SiteManagementSystem(SoftwareEngineering)/appsettings.json +++ b/SiteManagementSystem(SoftwareEngineering)/appsettings.json @@ -6,8 +6,10 @@ } }, "ConnectionStrings": { - "SQL": "Server=127.0.0.1;Port=3306;Database=SiteManagementSystem;Uid=SiteManagementSystem;Pwd=SiteManagementSystem;" + "Redis": "host.docker.internal:6379,abortConnect=false", + "SQL": "Server=host.docker.internal;Port=3306;Database=SiteManagementSystem;Uid=SiteManagementSystem;Pwd=SiteManagementSystem;" }, + "RedisDB": 0, "Jwt": { "SecurityKey": "TheSecretKeyForIwutMail,RandomInfo:z#$WX%ec56rv^b8n", "Issuer": "iwut-Mail",