[merge] merge and solve conflict
This commit is contained in:
commit
92f58a5b6f
|
@ -1,6 +1,7 @@
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Configuration;
|
using SiteManagementSystem_SoftwareEngineering_.Configuration;
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Factory;
|
using SiteManagementSystem_SoftwareEngineering_.Factory;
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Interface;
|
using SiteManagementSystem_SoftwareEngineering_.Interface;
|
||||||
|
using SiteManagementSystem_SoftwareEngineering_.Model;
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Service;
|
using SiteManagementSystem_SoftwareEngineering_.Service;
|
||||||
using static SiteManagementSystem_SoftwareEngineering_.Service.UserManagerService;
|
using static SiteManagementSystem_SoftwareEngineering_.Service.UserManagerService;
|
||||||
|
|
||||||
|
@ -24,11 +25,16 @@ namespace SiteManagementSystem_SoftwareEngineering_.Extension
|
||||||
services.AddScoped<ITokenFactory, TokenFactory>(_ => new TokenFactory(config));
|
services.AddScoped<ITokenFactory, TokenFactory>(_ => new TokenFactory(config));
|
||||||
return services;
|
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();
|
var config = new SecretConfig();
|
||||||
options(config);
|
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 =>
|
services.AddScoped<IUserManageService, UserManageService>(services =>
|
||||||
{
|
{
|
||||||
var logger = services.GetRequiredService<ILogger<UserManageService>>();
|
var logger = services.GetRequiredService<ILogger<UserManageService>>();
|
||||||
|
@ -37,5 +43,20 @@ namespace SiteManagementSystem_SoftwareEngineering_.Extension
|
||||||
});
|
});
|
||||||
return services;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace SiteManagementSystem_SoftwareEngineering_.Model
|
||||||
|
{
|
||||||
|
public class RedisConfig
|
||||||
|
{
|
||||||
|
public int RedisDB { get; set; }
|
||||||
|
public string RedisConnectionString { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Extension;
|
using SiteManagementSystem_SoftwareEngineering_.Extension;
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Interface;
|
using SiteManagementSystem_SoftwareEngineering_.Interface;
|
||||||
|
@ -53,7 +54,13 @@ builder
|
||||||
})
|
})
|
||||||
.AddUserManager(options =>
|
.AddUserManager(options =>
|
||||||
options.HashSalt = builder.Configuration.GetValue<string>("SecretSalt")!
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|
|
@ -22,9 +22,9 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service
|
||||||
private readonly ScriptEngine _engine = Python.CreateEngine();
|
private readonly ScriptEngine _engine = Python.CreateEngine();
|
||||||
private readonly dynamic _service;
|
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 _engine = Python.CreateEngine();
|
||||||
var searchPaths = _engine.GetSearchPaths();
|
var searchPaths = _engine.GetSearchPaths();
|
||||||
searchPaths.Add(@"./Service/PythonServiceFile/");
|
searchPaths.Add(@"./Service/PythonServiceFile/");
|
||||||
|
@ -36,6 +36,11 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service
|
||||||
_service = scope.GetVariable("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)
|
public void AddField(string name, string value)
|
||||||
{
|
{
|
||||||
var t = (Field)
|
var t = (Field)
|
||||||
|
@ -65,14 +70,26 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service
|
||||||
{
|
{
|
||||||
public readonly SQLHelperService<Field> FieldDb;
|
public readonly SQLHelperService<Field> FieldDb;
|
||||||
public readonly SQLHelperService<FieldRecord> RecordDb;
|
public readonly SQLHelperService<FieldRecord> 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));
|
FieldDb = new SQLHelperService<Field>(storageService, nameof(storageService.Fields));
|
||||||
RecordDb = new SQLHelperService<FieldRecord>(
|
RecordDb = new SQLHelperService<FieldRecord>(
|
||||||
storageService,
|
storageService,
|
||||||
nameof(storageService.UserFieldRecords)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import clr
|
import clr
|
||||||
from user import User
|
from user import User
|
||||||
from field import Field
|
from field import Field
|
||||||
from user_field import FieldRecord
|
from user_field import FieldRecord
|
||||||
|
@ -11,6 +11,7 @@ class Layer:
|
||||||
def __init__(self, layer):
|
def __init__(self, layer):
|
||||||
self._field_db = layer.FieldDb
|
self._field_db = layer.FieldDb
|
||||||
self._record_db = layer.RecordDb
|
self._record_db = layer.RecordDb
|
||||||
|
self._finished_field_recoed_db = layer.FinishedFieldRecordDb
|
||||||
|
|
||||||
def add_field(self, field: Field) -> 'Layer':
|
def add_field(self, field: Field) -> 'Layer':
|
||||||
self._field_db.Add(field.parse_to_csharp_object(self._field_db.GetDefaultEntity()))
|
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':
|
def remove_record(self, record: FieldRecord) -> 'Layer':
|
||||||
self._record_db.TryDelete("Uid", record.parse_to_csharp_object(self._record_db.GetDefaultEntity()).Uid)
|
self._record_db.TryDelete("Uid", record.parse_to_csharp_object(self._record_db.GetDefaultEntity()).Uid)
|
||||||
return self
|
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
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using SiteManagementSystem_SoftwareEngineering_.Entity;
|
using SiteManagementSystem_SoftwareEngineering_.Entity;
|
||||||
|
|
||||||
namespace SiteManagementSystem_SoftwareEngineering_.Service
|
namespace SiteManagementSystem_SoftwareEngineering_.Service
|
||||||
|
@ -11,8 +10,10 @@ namespace SiteManagementSystem_SoftwareEngineering_.Service
|
||||||
{
|
{
|
||||||
Database.EnsureCreated();
|
Database.EnsureCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
public DbSet<Field> Fields { get; set; }
|
public DbSet<Field> Fields { get; set; }
|
||||||
public DbSet<FieldRecord> UserFieldRecords { get; set; }
|
public DbSet<FieldRecord> UserFieldRecords { get; set; }
|
||||||
|
public DbSet<FieldRecord> FinishedFieldRecords { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
<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="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.7" />
|
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.4.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"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": {
|
"Jwt": {
|
||||||
"SecurityKey": "TheSecretKeyForIwutMail,RandomInfo:z#$WX%ec56rv^b8n",
|
"SecurityKey": "TheSecretKeyForIwutMail,RandomInfo:z#$WX%ec56rv^b8n",
|
||||||
"Issuer": "iwut-Mail",
|
"Issuer": "iwut-Mail",
|
||||||
|
|
Loading…
Reference in New Issue