From e084b2584b6ee20630910d5c39632d5e7e531470 Mon Sep 17 00:00:00 2001
From: lichx <lchx751176501@gmail.com>
Date: Sun, 3 Nov 2024 21:58:05 +0800
Subject: [PATCH] =?UTF-8?q?[feature]=20=E6=B7=BB=E5=8A=A0Redis=20=E7=9B=B8?=
 =?UTF-8?q?=E5=85=B3=E6=9C=8D=E5=8A=A1=20=E6=9C=AA=E5=BA=94=E7=94=A8?=
 =?UTF-8?q?=E5=88=B0python?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../Extension/IServiceCollectionExtension.cs  |  27 +++-
 .../Interface/ICacheService.cs                |  12 ++
 .../Model/RedisConfig.cs                      |   8 ++
 .../Program.cs                                |   9 +-
 .../Service/FieldService.cs                   |  23 +++-
 .../Service/PythonServiceFile/layer.py        |  16 ++-
 .../Service/RedisService.cs                   | 116 ++++++++++++++++++
 .../Service/SQLService.cs                     |   7 +-
 ...nagementSystem(SoftwareEngineering).csproj |   1 +
 .../appsettings.json                          |   2 +
 10 files changed, 210 insertions(+), 11 deletions(-)
 create mode 100644 SiteManagementSystem(SoftwareEngineering)/Interface/ICacheService.cs
 create mode 100644 SiteManagementSystem(SoftwareEngineering)/Model/RedisConfig.cs
 create mode 100644 SiteManagementSystem(SoftwareEngineering)/Service/RedisService.cs

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<ITokenFactory, TokenFactory>(_ => new TokenFactory(config));
             return services;
         }
-        public static IServiceCollection AddUserManager(this IServiceCollection services, Action<SecretConfig> options)
+
+        public static IServiceCollection AddUserManager(
+            this IServiceCollection services,
+            Action<SecretConfig> 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<IUserManageService, UserManageService>(services =>
             {
                 var logger = services.GetRequiredService<ILogger<UserManageService>>();
-                var storageService= services.GetRequiredService<SQLService>();
+                var storageService = services.GetRequiredService<SQLService>();
                 return new UserManageService(storageService, logger, config.HashSalt);
             });
             return services;
         }
+
+        public static IServiceCollection AddRedisUtils(
+            this IServiceCollection services,
+            Action<RedisConfig> options
+        )
+        {
+            var config = new RedisConfig();
+            options(config);
+            if (config.RedisConnectionString is null)
+                throw new ArgumentNullException("Redis ConnectionString is null.");
+            services.AddSingleton<RedisUtils>(services =>
+                new RedisUtils(config.RedisConnectionString, config.RedisDB, services.GetRequiredService<ILogger<RedisUtils>>())
+            );
+            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<TCategoryName>
+    {
+        public Action<string>? ExpiredCallbackFunc { set; }
+        public int Capacity { get; set; }
+        public Task<bool> SetAsync(string key, string value, TimeSpan? expiry = null);
+        public Task<string?> GetAsync(string key);
+        public Task<bool> RemoveAsync(string key);
+        public Task<bool> 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 76f1414..51b29e8 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;
@@ -52,7 +53,13 @@ builder
     })
     .AddUserManager(options =>
         options.HashSalt = builder.Configuration.GetValue<string>("SecretSalt")!
-    ).AddScoped<IFieldService, FieldService>();
+    ).AddScoped<IFieldService, FieldService>()
+    .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 d993585..6c82613 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,ICacheService<FieldService>cacheService)
         {
-            _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<Field> FieldDb;
         public readonly SQLHelperService<UserField> RecordDb;
+        public readonly SQLHelperService<Field> FinishedFieldRecordDb;
+        private readonly ICacheService<FieldService> _cacheService;
 
-        public EntityFrameworkPythonCompatibilityAndInterpretationLayer(SQLService storageService)
+        public EntityFrameworkPythonCompatibilityAndInterpretationLayer(SQLService storageService, ICacheService<FieldService> cacheService)
         {
+            _cacheService = cacheService;
             FieldDb = new SQLHelperService<Field>(storageService, nameof(storageService.Fields));
             RecordDb = new SQLHelperService<UserField>(
                 storageService,
                 nameof(storageService.UserFieldRecords)
             );
+            FinishedFieldRecordDb = new SQLHelperService<Field>(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 b2e853e..2d53fcf 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 UserFieldRecord
@@ -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: UserFieldRecord) -> '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<TCategoryName>(ILogger<RedisService<TCategoryName>> logger, RedisUtils redisHelper) : ICacheService<TCategoryName>
+    {
+        private readonly ILogger<RedisService<TCategoryName>> _logger = logger;
+        private readonly RedisUtils _redisHelper = redisHelper;
+        private readonly LinkedList<string> _values = [];
+        private readonly string _categoryName = typeof(TCategoryName).Name;
+        public int Capacity { get; set; } = int.MaxValue;
+        private Action<string>? _expiredCallbackFunc = null;
+        public Action<string>? ExpiredCallbackFunc
+        {
+            set
+            {
+                _expiredCallbackFunc = value;
+                _redisHelper.SetExpiredCallbackFunction(_categoryName, KeyDrop);
+            }
+        }
+
+        public async Task<bool> 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<string?> GetAsync(string key) =>
+            await _redisHelper.GetAsync(_categoryName, key);
+
+        public async Task<bool> RemoveAsync(string key) =>
+            await _redisHelper.RemoveAsync(_categoryName, key);
+
+        /// <summary>
+        /// 删除现有的最早加入的数据
+        /// </summary>
+        /// <returns></returns>
+        public async Task<bool> 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<RedisUtils> _logger;
+        private readonly Dictionary<string, Action<string>?> _expiredCallbackFunctions = [];
+        private readonly IDatabase _database;
+        private readonly int _numberOfDB;
+
+        public RedisUtils(string connectionString,int redisDB, ILogger<RedisUtils> 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<bool> SetAsync(
+            string typeName,
+            string key,
+            string value,
+            TimeSpan? expiry = null
+        ) => await _database.StringSetAsync(typeName + " " + key, value, expiry);
+
+        public async Task<string?> GetAsync(string typeName, string key) =>
+            await _database.StringGetAsync(typeName + " " + key);
+
+        public async Task<bool> RemoveAsync(string typeName, string key) =>
+            await _database.KeyDeleteAsync(typeName + " " + key);
+
+        public void SetExpiredCallbackFunction(string typeName, Action<string>? 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 b8dfd16..bf39a7c 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<User> Users { get; set; }
         public DbSet<Field> Fields { get; set; }
-        public DbSet<UserField> UserFieldRecords{get;set;}
+        public DbSet<UserField> UserFieldRecords { get; set; }
+        public DbSet<UserField> FinishedFieldRecords { get; set; }
     }
 }
diff --git a/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj b/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj
index d88f9e9..32667f5 100644
--- a/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj
+++ b/SiteManagementSystem(SoftwareEngineering)/SiteManagementSystem(SoftwareEngineering).csproj
@@ -14,6 +14,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
     <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
+    <PackageReference Include="StackExchange.Redis" Version="2.8.16" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
     <PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.7" />
   </ItemGroup>
diff --git a/SiteManagementSystem(SoftwareEngineering)/appsettings.json b/SiteManagementSystem(SoftwareEngineering)/appsettings.json
index a41b3be..19222d0 100644
--- a/SiteManagementSystem(SoftwareEngineering)/appsettings.json
+++ b/SiteManagementSystem(SoftwareEngineering)/appsettings.json
@@ -6,8 +6,10 @@
     }
   },
   "ConnectionStrings": {
+    "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",