初始化项目

This commit is contained in:
huzhiyun 2026-02-24 16:03:01 +08:00
parent bead790d60
commit 7da90e0b51
74 changed files with 4192 additions and 0 deletions

6
YuanXuan.IM.Api.slnx Normal file
View File

@ -0,0 +1,6 @@
<Solution>
<Project Path="YuanXuan.IM.Api/YuanXuan.IM.Api.csproj" />
<Project Path="YuanXuan.IM.Common/YuanXuan.IM.Common.csproj" />
<Project Path="YuanXuan.IM.Core/YuanXuan.IM.Core.csproj" />
<Project Path="YuanXuan.IM.Infrastructure/YuanXuan.IM.Infrastructure.csproj" />
</Solution>

View File

@ -0,0 +1,37 @@
using Asp.Versioning;
namespace YuanXuan.IM.Api.CollectionExtensions
{
/// <summary>
/// Api版本控制服务扩展类
/// </summary>
public static class ApiVersioningServiceCollectionExtensions
{
/// <summary>
/// 添加Api版本控制服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddApiVersion(this IServiceCollection services)
{
services.AddApiVersioning(config =>
{
// 默认的Api版号
config.DefaultApiVersion = new ApiVersion(1, 0);
// 未指定Api版号的时候使用默认版号
config.AssumeDefaultVersionWhenUnspecified = true;
// 是否在返回响应头中返回Api版本信息
config.ReportApiVersions = true;
}).AddApiExplorer(options =>
{
// Api 版本分组名称
options.GroupNameFormat = "'v'VVV";
// 未指定Api版号的时候使用默认版号
options.SubstituteApiVersionInUrl = true;
});
return services;
}
}
}

View File

@ -0,0 +1,198 @@
using Masuit.Tools;
using Microsoft.Extensions.DependencyModel;
using Serilog;
using System.Reflection;
using System.Runtime.Loader;
using YuanXuan.IM.Common.Attributes;
namespace YuanXuan.IM.Api.CollectionExtensions
{
/// <summary>
/// 批量注册服务
/// </summary>
public static class BatchRegisterServiceCollectionExtension
{
/// <summary>
/// 批量注册带属性的服务和后台服务
/// </summary>
/// <param name="services"></param>
public static void BatchRegisterServices(this IServiceCollection services)
{
var allAssembly = GetAllAssembly();
services.RegisterServiceByAttribute(ServiceLifetime.Singleton, allAssembly);
services.RegisterServiceByAttribute(ServiceLifetime.Scoped, allAssembly);
services.RegisterServiceByAttribute(ServiceLifetime.Transient, allAssembly);
services.RegisterBackgroundService(allAssembly);
}
/// <summary>
/// 通过 InjectAttribute 批量注册服务
/// </summary>
/// <param name="services"></param>
/// <param name="serviceLifetime"></param>
private static void RegisterServiceByAttribute(this IServiceCollection services, ServiceLifetime serviceLifetime, List<Assembly> allAssembly)
{
var types = allAssembly.SelectMany(t => t.GetTypes())
.Where(t => t.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0 &&
t.GetCustomAttribute<InjectAttribute>()?.Lifetime == serviceLifetime &&
t.IsClass && !t.IsAbstract).ToList();
foreach (var type in types)
{
var baseInterfaces = type.BaseType?.GetInterfaces();
Type? typeInterface = null;
// 获取所有接口然后过滤掉系统接口和常见的不适合DI的接口
var allInterfaces = type.GetInterfaces();
var filteredInterfaces = allInterfaces.Where(IsCustomInterface).ToList();
// 如果没有自定义接口,则使用直接注入
if (filteredInterfaces.Count == 0)
{
//服务非继承自接口的直接注入
switch (serviceLifetime)
{
case ServiceLifetime.Singleton: services.AddSingleton(type); break;
case ServiceLifetime.Scoped: services.AddScoped(type); break;
case ServiceLifetime.Transient: services.AddTransient(type); break;
}
Log.Information("直接注入服务: {TypeName} (生命周期: {Lifetime})", type.Name, serviceLifetime);
}
else
{
// 如果有多个自定义接口,选择最合适的接口
typeInterface = ChooseBestInterface(filteredInterfaces, type);
//服务继承自接口的和接口一起注入
switch (serviceLifetime)
{
case ServiceLifetime.Singleton: services.AddSingleton(typeInterface, type); break;
case ServiceLifetime.Scoped: services.AddScoped(typeInterface, type); break;
case ServiceLifetime.Transient: services.AddTransient(typeInterface, type); break;
}
Log.Information("接口注入服务: {TypeName} -> {InterfaceName} (生命周期: {Lifetime})",
type.Name, typeInterface.Name, serviceLifetime);
}
}
}
/// <summary>
/// 注册后台服务
/// </summary>
/// <param name="services"></param>
/// <param name="serviceLifetime"></param>
private static void RegisterBackgroundService(this IServiceCollection services, List<Assembly> allAssembly)
{
List<Type> types = allAssembly.SelectMany(t => t.GetTypes()).Where(t => typeof(BackgroundService).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract).ToList();
foreach (var type in types)
{
services.AddSingleton(typeof(IHostedService), type);
}
}
/// <summary>
/// 判断是否为自定义接口过滤掉系统接口和常见的不适合DI的接口
/// </summary>
/// <param name="interfaceType">接口类型</param>
/// <returns></returns>
private static bool IsCustomInterface(Type interfaceType)
{
// 系统命名空间的接口不适合DI
var systemNamespaces = new[]
{
"System",
"Microsoft",
"Windows",
"IDisposable",
"IComparable",
"IEquatable",
"IEnumerable",
"ICollection",
"IList",
"IAsyncEnumerable",
"IAsyncDisposable"
};
// 如果接口名称包含系统命名空间或常见系统接口名称,则过滤掉
if (systemNamespaces.Any(ns => interfaceType.Namespace?.StartsWith(ns) == true ||
interfaceType.Name.StartsWith(ns)))
{
return false;
}
// 检查接口是否有自定义项目的命名空间
var customNamespaces = new[]
{
"LearningOfficer.OA",
"LearningOfficer"
};
// 如果接口有自定义项目的命名空间,则认为是自定义接口
return customNamespaces.Any(ns => interfaceType.Namespace?.StartsWith(ns) == true);
}
/// <summary>
/// 从多个自定义接口中选择最合适的接口
/// </summary>
/// <param name="interfaces">接口列表</param>
/// <param name="implementationType">实现类型</param>
/// <returns></returns>
private static Type ChooseBestInterface(List<Type> interfaces, Type implementationType)
{
// 如果只有一个接口,直接返回
if (interfaces.Count == 1)
return interfaces[0];
// 优先选择名称匹配的接口例如MyService -> IMyService
var serviceName = implementationType.Name;
var preferredInterface = interfaces.FirstOrDefault(i =>
i.Name == "I" + serviceName ||
i.Name == serviceName.Replace("Service", "").Replace("Impl", "") + "Service");
if (preferredInterface != null)
return preferredInterface;
// 按接口名称排序,选择最合适的
return interfaces
.OrderBy(i => i.Name.StartsWith("I") ? 0 : 1) // 优先选择 I 开头的接口
.ThenBy(i => i.Name.Length) // 选择名称较短的接口
.First();
}
/// <summary>
/// 获取全部 Assembly
/// </summary>
/// <returns></returns>
private static List<Assembly> GetAllAssembly()
{
var allAssemblies = new List<Assembly>();
var loadedAssemblies = new HashSet<string>();
var dependencyContext = DependencyContext.Default;
var libraries = dependencyContext.RuntimeLibraries
.Where(lib => !lib.Serviceable && lib.Type != "package")
.ToList();
foreach (var library in libraries)
{
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(library.Name));
if (loadedAssemblies.Add(assembly.FullName))
{
allAssemblies.Add(assembly);
}
}
catch (Exception)
{
Log.Fatal("加载程序集失败:{0}", library.Name);
// Log or handle the exception if necessary
}
}
return allAssemblies;
}
}
}

View File

@ -0,0 +1,28 @@
using YuanXuan.IM.Common.Configs;
using YuanXuan.IM.Common.Dtos.ALiYun;
namespace YuanXuan.IM.Api.CollectionExtensions
{
/// <summary>
/// 强类型配置服务
/// </summary>
public static class ConfigureOptionServiceCollectionExtension
{
/// <summary>
/// 添加强类型配置服务
/// </summary>
/// <param name="services"></param>
public static void AddConfigureOptions(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<NacosConfig>(configuration.GetSection("nacos"));
services.AddOptions();
services.Configure<OSSConfigResult>(configuration.GetSection("OSSConfig"));
services.Configure<ImConfig>(configuration.GetSection("ImConfig"));
services.Configure<HangFireSettings>(configuration.GetSection("HangFireSettings"));
services.Configure<UpAppVersionConfig>(configuration.GetSection("UpAppVersion"));
services.Configure<RabbitMQConfig>(configuration.GetSection("RabbitMQ"));
}
}
}

View File

@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using YuanXuan.IM.Common.Configs;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class JWTAuthServiceCollectionExtensions
{
/// <summary>
/// Jwt认证服务
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration)
{
//将配置文件中的相关内容反序列化
var jwtOption = configuration.GetSection("Jwt").Get<JwtSettings>();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
//验证失败时的处理
OnAuthenticationFailed = context =>
{
//若失败类型为过期则返回特定Header便于客户端判断
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
context.Response.Headers.Add("tokenErr", "expired");
return Task.CompletedTask;
}
//// 配置 SignalR 使用 JWT
//,OnMessageReceived = context =>
//{
// var accessToken = context.Request.Query["access_token"];
// var path = context.HttpContext.Request.Path;
// if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/signalr"))
// {
// context.Token = accessToken;
// }
// return Task.CompletedTask;
//}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true, //是否验证Issuer
ValidIssuer = jwtOption.Issuer, //发行人Issuer
ValidateAudience = true, //是否验证Audience
ValidAudience = jwtOption.Audience, //订阅人Audience
ValidateIssuerSigningKey = true, //是否验证SecurityKey
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOption.AccessSecret)), //SecurityKey
ValidateLifetime = true, //是否验证失效时间
ClockSkew = TimeSpan.FromSeconds(jwtOption.ClockSkew), //过期时间容错值,解决服务器端时间不同步问题(秒)
RequireExpirationTime = true,
};
});
services.AddAuthorization();
return services;
}
}
}

View File

@ -0,0 +1,23 @@
using Nacos.V2.DependencyInjection;
namespace YuanXuan.IM.Api.CollectionExtensions
{
/// <summary>
/// 强类型配置服务
/// </summary>
public static class NacosServiceCollectionExtension
{
/// <summary>
/// 添加强类型配置服务
/// </summary>
/// <param name="services"></param>
public static void AddNacos(this IServiceCollection services, IConfiguration configuration, IHostBuilder host)
{
services.AddNacosV2Config(configuration);
services.AddNacosV2Naming(configuration);
host.UseNacosConfig("nacos");
//services.AddNacosAspNet(configuration, section: "nacos");
}
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class PollyServiceCollectionExtensions
{
/// <summary>
/// 添加Polly策略
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddPollyPolicies(this IServiceCollection services)
{
// 添加HTTP客户端工厂
services.AddHttpClient("YarpClient")
.ConfigurePrimaryHttpMessageHandler(() => new System.Net.Http.HttpClientHandler());
return services;
}
}
}

View File

@ -0,0 +1,22 @@
using YuanXuan.IM.Common.Configs;
using YuanXuan.IM.Infrastructure.Redis;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class RedisServiceCollectionExtensions
{
/// <summary>
/// 添加Redis服务
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration)
{
var connectionStrings = configuration.GetSection("ConnectionStrings").Get<ConnectionStringsSettings>();
RedisHelper.Initialization(connectionStrings.Redis);
return services;
}
}
}

View File

@ -0,0 +1,57 @@
using Serilog;
using Serilog.Sinks.Grafana.Loki;
using System;
using YuanXuan.IM.Api.CollectionExtensions;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class SerilogServiceCollectionExtensions
{
/// <summary>
/// 添加Serilog
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddSerilog(this IServiceCollection services, IConfiguration configuration, IHostBuilder hostBuilder, IHostEnvironment environment)
{
var labels = new List<LokiLabel>
{
new LokiLabel
{
Key="app",
Value=environment.ApplicationName
},
new LokiLabel
{
Key="env",
Value=environment.EnvironmentName
},
};
// 配置 Serilog
var logger = new LoggerConfiguration()
.WriteTo.Console()
.Enrich.FromLogContext();
//if (environment.IsDevelopment())
//{
logger.MinimumLevel.Information()
.WriteTo.GrafanaLoki(configuration["GrafanaLoki:LokiUri"], labels, new List<string>()
{
//"RequestId","Path"
}, tenant: configuration["GrafanaLoki:TenantId"]);
//}
//else
//{
// logger.MinimumLevel.Information()
// .WriteTo.GrafanaLoki(configuration["GrafanaLoki:LokiUri"], labels);
//}
Log.Logger = logger.CreateLogger();
services.AddSerilog(Log.Logger);
return services;
}
}
}

View File

@ -0,0 +1,117 @@
using Serilog;
using SqlSugar;
using System.Text.RegularExpressions;
using Yitter.IdGenerator;
using YuanXuan.IM.Common.Configs;
using YuanXuan.IM.Infrastructure.DBContext;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class SqlSugarServiceCollectionExtensions
{
/// <summary>
/// 添加SqlSugar
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddSqlSugar(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment env)
{
var connectionStrings = configuration.GetSection("ConnectionStrings").Get<ConnectionStringsSettings>();
//YitIdHelper.SetIdGenerator(new IdGeneratorOptions(configuration.GetValue<ushort>("SnowFlakeWorkId")));
// 自定义雪花ID算法 程序启动时执行一次就行
StaticConfig.CustomSnowFlakeFunc = () =>
{
return YitIdHelper.NextId();
};
services.AddScoped<ISqlSugarClient>(provider =>
{
var serviceProvider = provider; // 获取 IServiceProvider
List<ConnectionConfig> configs = new()
{
new ConnectionConfig
{
DbType = DbType.MySql,
ConfigId = nameof(connectionStrings.Db),
ConnectionString = connectionStrings?.Db ?? throw new InvalidOperationException("Connection string cannot be null."),
IsAutoCloseConnection = true,
AopEvents = SetAopEvents(connectionStrings.Db, env.EnvironmentName, serviceProvider),
},
new ConnectionConfig
{
DbType = DbType.MySql,
ConfigId = nameof(connectionStrings.UserCenterDb),
ConnectionString = connectionStrings?.UserCenterDb ?? throw new InvalidOperationException("Connection string cannot be null."),
IsAutoCloseConnection = true,
AopEvents = SetAopEvents(connectionStrings.UserCenterDb, env.EnvironmentName, serviceProvider),
}
};
return new SqlSugarClient(configs);
});
services.AddScoped(typeof(SugarRepository<>));
return services;
}
private static AopEvents SetAopEvents(string connectionString, string environmentName, IServiceProvider serviceProvider)
{
var aopEvents = new AopEvents();
if (environmentName == Environments.Development)
{
// 正则表达式匹配Ip
var ipMatch = Regex.Match(connectionString, @"((25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))");
var connections = connectionString.Split(';');
string dbNamne = string.Empty;
foreach (var item in connections)
{
if (item.Contains("Database"))
{
dbNamne = item.Split('=')[1];
}
}
aopEvents.OnLogExecuting = (sql, pars) =>
{
// 打印 Sql 语句
Console.WriteLine($"执行Sql:{Environment.NewLine}{UtilMethods.GetNativeSql(sql, pars)}");
Log.Logger.Debug($"执行Sql:{Environment.NewLine}{UtilMethods.GetNativeSql(sql, pars)}");
};
}
//aopEvents.DataExecuting += (oldValue, entityInfo) =>
//{
// // 获取 ICurrentUserService
// var currentUserService = serviceProvider.GetService<ICurrentUserService>();
// //if (entityInfo.EntityColumnInfo.IsPrimarykey && entityInfo.EntityValue is BaseEntity baseEntity)
// //{
// // if (entityInfo.OperationType == DataFilterType.InsertByObject)
// // {
// // // 插入时填充创建信息
// // baseEntity.CreatedUserId = currentUserService.GetUserId();
// // baseEntity.CreatedUserName = currentUserService.GetUserName();
// // baseEntity.CreatedUserRealname = currentUserService.GetUserName();
// // }
// // else if (entityInfo.OperationType == DataFilterType.UpdateByObject)
// // {
// // // 更新时填充修改信息
// // baseEntity.ModifiedUserId = currentUserService.GetUserId();
// // baseEntity.ModifiedUserName = currentUserService.GetUserName();
// // baseEntity.ModifiedUserRealname = currentUserService.GetUserName();
// // }
// //}
//};
return aopEvents;
}
}
}

View File

@ -0,0 +1,87 @@
using LearningOfficer.OA.Mobile.Api.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class SwaggerServiceCollectionExtensions
{
/// <summary>
/// Swagger注入
/// </summary>
/// <param name="services"></param>
public static void AddSwagger(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "OA移动端Api",
Version = "v1"
});
// v2 文档
c.SwaggerDoc("v2", new OpenApiInfo
{
Title = "OA移动端Api",
Version = "v2"
});
// v3 文档
c.SwaggerDoc("v3", new OpenApiInfo
{
Title = "OA移动端Api",
Version = "v3"
});
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Description = "在下框中输入请求头中需要添加Jwt授权TokenBearer {Token},注意中间有空格",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
BearerFormat = "JWT",
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
c.SupportNonNullableReferenceTypes();
c.ParameterFilter<NullableParameterFilter>();
// 获取主项目生成的 XML 文件路径
var mainXmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var mainXmlPath = Path.Combine(AppContext.BaseDirectory, mainXmlFile);
c.IncludeXmlComments(mainXmlPath, includeControllerXmlComments: true);
// 获取所有引用的类库 XML 文件路径
var referencedAssemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
foreach (var assemblyName in referencedAssemblies)
{
var libraryXmlPath = Path.Combine(AppContext.BaseDirectory, $"{assemblyName.Name}.xml");
if (File.Exists(libraryXmlPath))
{
c.IncludeXmlComments(libraryXmlPath);
}
}
});
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Nacos.V2;
using Nacos.V2.Naming;
using Nacos.V2.Naming.Dtos;
using Newtonsoft.Json;
using System.Threading;
using Yarp.ReverseProxy.Configuration;
using YuanXuan.IM.Api.Proxy;
namespace YuanXuan.IM.Api.CollectionExtensions
{
public static class YarpServiceCollectionExtensions
{
/// <summary>
/// 添加YARP反向代理服务
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static IServiceCollection AddYarpWithNacos(this IServiceCollection services, IConfiguration configuration)
{
// 添加YARP服务
services.AddReverseProxy();
// 注册Nacos服务发现提供者
services.AddSingleton<NacosProxyConfigProvider>();
services.AddSingleton<IProxyConfigProvider>(provider => provider.GetRequiredService<NacosProxyConfigProvider>());
return services;
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using YuanXuan.IM.Common.Dtos.LoginMobile;
using YuanXuan.IM.Common.Response;
namespace YuanXuan.IM.Api.Controllers
{
/// <summary>
/// 自定义基础Api控制器
/// </summary>
[Authorize]
[ApiController]
public class BaseApiController : ControllerBase
{
/// <summary>
/// Api版本前缀
/// </summary>
protected const string RoutePrefix = "api/v{version:apiVersion}";
public MobileTokenInfo ZereUser => GetTokenInfo();
private MobileTokenInfo GetTokenInfo()
{
return new MobileTokenInfo
{
UserName = HttpContext?.User?.FindFirst("UserName")?.Value,
UserId = HttpContext?.User?.FindFirst("UserId")?.Value,
Jwt_id = HttpContext?.User?.FindFirst("Jwt_id")?.Value,
};
}
/// <summary>
/// 返回成功结果
/// </summary>
/// <param name="data">数据</param>
/// <returns></returns>
protected IActionResult Success(object data = null)
{
return Ok(new BaseResponse<object>(200, "success", data));
}
/// <summary>
/// 返回失败结果
/// </summary>
/// <param name="message">错误信息</param>
/// <returns></returns>
protected IActionResult Fail(string message)
{
return BadRequest(new BaseResponse<object>(400, message));
}
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace YuanXuan.IM.Api.Controllers
{
/// <summary>
/// 健康检查控制器
/// </summary>
[Route("[controller]")]
[ApiController]
public class HealthController : ControllerBase
{
/// <summary>
/// 健康检查
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public IActionResult Check()
{
return Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow });
}
}
}

View File

@ -0,0 +1,120 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using YuanXuan.IM.Common.Dtos.LoginMobile;
using YuanXuan.IM.Common.Helpers;
using YuanXuan.IM.Infrastructure.Redis;
namespace YuanXuan.IM.Api.Controllers
{
/// <summary>
/// 登录授权控制器
/// </summary>
[Route($@"{RoutePrefix}/[controller]/[action]")]
[ApiVersion(1.0)]
public class LoginAuthorController : BaseApiController
{
/// <summary>
/// 登录
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
// 这里应该添加实际的登录验证逻辑
// 暂时模拟登录成功
var userId = "123456";
var userName = "testuser";
// 生成JWT token
var token = JwtHelper.GenerateToken(userId, userName);
var refreshToken = JwtHelper.GenerateRefreshToken();
// 存储token到Redis用于后续的验证和登出
await RedisHelper.SetAsync($"user:token:{userId}", token, TimeSpan.FromHours(24));
await RedisHelper.SetAsync($"user:refreshToken:{userId}", refreshToken, TimeSpan.FromDays(7));
return Success(new { Token = token, RefreshToken = refreshToken, UserId = userId, UserName = userName });
}
/// <summary>
/// 刷新Token
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
// 验证refreshToken
var principal = JwtHelper.GetPrincipalFromExpiredToken(request.Token);
var userId = principal?.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
if (userId == null)
{
return Fail("无效的token");
}
var storedRefreshToken = await RedisHelper.GetStringAsync($"user:refreshToken:{userId}");
if (storedRefreshToken != request.RefreshToken)
{
return Fail("无效的refreshToken");
}
var userName = principal?.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
var newToken = JwtHelper.GenerateToken(userId, userName);
var newRefreshToken = JwtHelper.GenerateRefreshToken();
await RedisHelper.SetAsync($"user:token:{userId}", newToken, TimeSpan.FromHours(24));
await RedisHelper.SetAsync($"user:refreshToken:{userId}", newRefreshToken, TimeSpan.FromDays(7));
return Success(new { Token = newToken, RefreshToken = newRefreshToken });
}
/// <summary>
/// 登出
/// </summary>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> Logout()
{
var userId = User.FindFirst("sub")?.Value;
if (userId == null)
{
return Fail("用户未登录");
}
// 从Redis中删除token
await RedisHelper.DeleteAsync($"user:token:{userId}");
await RedisHelper.DeleteAsync($"user:refreshToken:{userId}");
return Success("登出成功");
}
/// <summary>
/// 全局登出(所有设备)
/// </summary>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> GlobalLogout()
{
var userId = User.FindFirst("sub")?.Value;
if (userId == null)
{
return Fail("用户未登录");
}
// 删除所有相关的token
await RedisHelper.DeleteAsync($"user:token:{userId}");
await RedisHelper.DeleteAsync($"user:refreshToken:{userId}");
// 这里可以添加更多的清理逻辑,比如删除所有设备的登录记录
return Success("全局登出成功");
}
}
}

View File

@ -0,0 +1,56 @@
using Newtonsoft.Json;
using System.Reflection;
using Newtonsoft.Json.Serialization;
using System.Text.RegularExpressions;
using System.Text;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
public class CustomContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (!string.IsNullOrEmpty(member.Name))
{
if (member.Name.Length >= 1 && char.IsUpper(member.Name[0]))
{
property.PropertyName = char.ToLower(member.Name[0]) + member.Name.Substring(1);
}
property.PropertyName = ChangeName(property.PropertyName);
}
return property;
}
/// <summary>
/// 将下划线命名转换为小驼峰命名
/// </summary>
/// <param name="name">变量名</param>
/// <returns></returns>
private string ChangeName(string name)
{
Match mt = Regex.Match(name, @"_(\w*)*");
if (mt.Success) {
var sb = new StringBuilder();
bool toUpper = false;
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '_')
{
toUpper = true;
}
else
{
sb.Append(toUpper ? char.ToUpperInvariant(c) : c);
toUpper = false;
}
}
if (sb.Length > 0)
sb[0] = char.ToLowerInvariant(sb[0]);
return sb.ToString();
}
return name;
}
}
}

View File

@ -0,0 +1,54 @@
using System.Runtime.Intrinsics.X86;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
public class CustomJsonNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
//第一个字母小写
if (string.IsNullOrEmpty(name))
{
return name;
}
if (name.Length > 1 && char.IsUpper(name[0]))
{
name = char.ToLower(name[0]) + name.Substring(1);
}
return ChangeName(name);
}
private string ChangeName(string name)
{
Match mt = Regex.Match(name, @"_(\w*)*");
if (mt.Success)
{
var sb = new StringBuilder();
bool toUpper = false;
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '_')
{
toUpper = true;
}
else
{
sb.Append(toUpper ? char.ToUpperInvariant(c) : c);
toUpper = false;
}
}
if (sb.Length > 0)
{
sb[0] = char.ToLowerInvariant(sb[0]);
}
return sb.ToString();
}
return name;
}
}
}

View File

@ -0,0 +1,135 @@
using System.Text.RegularExpressions;
using System.Text;
using Mapster;
using Masuit.Tools;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using YuanXuan.IM.Common.Response;
using YuanXuan.IM.Common.Exceptions;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
/// <summary>
/// 全局异常捕获过滤器
/// </summary>
public class GlobalExceptionCatchFilter : IAsyncExceptionFilter
{
private readonly ILogger<GlobalExceptionCatchFilter> _logger;
public GlobalExceptionCatchFilter(ILogger<GlobalExceptionCatchFilter> logger)
{
_logger = logger; //在构造函数中注入日志处理实例
}
public async Task OnExceptionAsync(ExceptionContext context)
{
// 如果异常没有被处理则进行处理
if (context.ExceptionHandled == false)
{
// 定义返回类型
BaseResponse<object> result;
// 如果为业务逻辑抛出的内部异常
if (context.Exception is BusinessException ex)
{
if (ex.BussinessExceptionData != null)
{
result = new BaseResponse<object>((int)ex.ErrorCode, context.Exception.Message, ex.BussinessExceptionData);
//将result的data转换为Dictionary<string, object>
var data = result.data.ToDictionary();
ConvertKeysToCamelCase(data);
result.data = data;
}
else
{
result = new BaseResponse<object>((int)ex.ErrorCode, context.Exception.Message);
}
}
else
{
// 程序异常,不对外暴露程序异常细节
result = new BaseResponse<object>((int)BusinessExceptionCode.AppError, "服务器好像出错了!"
// ,new
//{
// RequestId = context.HttpContext.TraceIdentifier
//}
);
//使用日志对象 _logger 的 LogError() 方法将异常信息写入日志文件
_logger.LogError(context.Exception, context.Exception.Message);
}
context.Result = new ContentResult
{
// 返回状态码设置为200表示成功
StatusCode = StatusCodes.Status200OK,
// 设置返回格式
ContentType = "application/json;charset=utf-8",
Content = result.ToJsonString()
};
}
// 设置为true表示异常已经被处理了
context.ExceptionHandled = true;
}
public static void ConvertKeysToCamelCase(Dictionary<string, object> dictionary)
{
var keys = dictionary.Keys.ToList();
foreach (var key in keys)
{
string ItemKey = key;
if (ItemKey.Length >= 1 && char.IsUpper(ItemKey[0]))
{
ItemKey = char.ToLower(ItemKey[0]) + ItemKey.Substring(1);
}
var camelCaseKey = ChangeName(ItemKey);
if (dictionary.ContainsKey(camelCaseKey))
{
dictionary[camelCaseKey] = dictionary[key];
}
else
{
dictionary.Add(camelCaseKey, dictionary[key]);
}
dictionary.Remove(key);
}
}
/// <summary>
/// 将下划线命名转换为小驼峰命名
/// </summary>
/// <param name="name">变量名</param>
/// <returns></returns>
private static string ChangeName(string name)
{
Match mt = Regex.Match(name, @"_(\w*)*");
if (mt.Success)
{
var sb = new StringBuilder();
bool toUpper = false;
for (int i = 0; i < name.Length; i++)
{
char c = name[i];
if (c == '_')
{
toUpper = true;
}
else
{
sb.Append(toUpper ? char.ToUpperInvariant(c) : c);
toUpper = false;
}
}
if (sb.Length > 0)
sb[0] = char.ToLowerInvariant(sb[0]);
return sb.ToString();
}
return name;
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using System.Text;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
public class GlobalOperationLogFilter : IAsyncActionFilter
{
private readonly ILogger<GlobalOperationLogFilter> _logger;
public GlobalOperationLogFilter(ILogger<GlobalOperationLogFilter> logger )
{
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//// 获取请求信息
var paramss = context.ActionDescriptor.Parameters;
var ActionName = ((ControllerActionDescriptor)context.ActionDescriptor)?.ActionName;
var Method = context.HttpContext.Request.Method;
var queryString = context.HttpContext.Request.QueryString;
var RequestUrl = context.HttpContext.Request.Host + context.HttpContext.Request.Path;
string token = context.HttpContext.Request.Headers["Authorization"].FirstOrDefault();
string body = "";
var request = context.HttpContext.Request;
var stream = request.Body;
if (request.ContentLength != null && request.ContentLength > 0)
{
request.EnableBuffering();
request.Body.Position = 0;//将读取指针迻到开始位置
using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true))
{
body = await reader.ReadToEndAsync();
}
}
_logger.LogInformation(context.HttpContext.TraceIdentifier + ": {RequestParams}", body);
await next();
}
private async Task<string> GetRequestBody(HttpRequest request)
{
request.EnableBuffering();
var body = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
request.Body.Position = 0; // 重置流位置
return Encoding.UTF8.GetString(buffer);
}
private async Task<string> GetResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
var body = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return body;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
public class NullableParameterFilter : IParameterFilter
{
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (context.ApiParameterDescription.Type is null)
return;
if (Nullable.GetUnderlyingType(context.ApiParameterDescription.Type) != null)
{
parameter.Schema.Nullable = true;
}
else if (context.ApiParameterDescription.Type.IsClass || context.ApiParameterDescription.Type.IsInterface)
{
var nullableAttribute = context.ParameterInfo?.GetCustomAttribute<System.Runtime.CompilerServices.NullableAttribute>();
if (nullableAttribute != null || context.ApiParameterDescription.Type.FullName?.Contains("System.Nullable") == true)
{
parameter.Schema.Nullable = true;
}
}
}
}
}

View File

@ -0,0 +1,199 @@
using Dm.util;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using NetTaste;
using Newtonsoft.Json;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Configs;
using YuanXuan.IM.Common.Dtos.LoginMobile;
using YuanXuan.IM.Common.Response;
using YuanXuan.IM.Infrastructure.Redis;
namespace LearningOfficer.OA.Mobile.Api.Filters
{
/// <summary>
/// 统一返回结果过滤器
/// </summary>
public class UniformResultActionFilter : ActionFilterAttribute
{
private readonly NacosConfig _nacosConfig;
private readonly ILogger<UniformResultActionFilter> _logger;
public UniformResultActionFilter(ILogger<UniformResultActionFilter> logger,IOptions<NacosConfig> nacosConfig)
{
_logger = logger;
_nacosConfig = nacosConfig.Value;
}
/// <summary>
/// 在Controller的Action执行后执行
/// </summary>
/// <param name="context"></param>
public override void OnActionExecuted(ActionExecutedContext context)
{
//特殊处理对有ApiResultIgnoreAttribute标签的不进行返回结果包装原样输出
//var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
//if (controllerActionDescriptor != null)
//{
// var isDefined = controllerActionDescriptor.EndpointMetadata.Any(a => a.GetType().Equals(typeof(ApiResultIgnoreAttribute)));
// if (isDefined)
// {
// return;
// }
//}
// 返回结果为JsonResult的请求进行Result包装
if (context.Result != null)
{
switch (context.Result)
{
case ObjectResult:
{
var result = context.Result as ObjectResult;
context.Result = new JsonResult(new BaseResponse<object>(200, "请求成功", result.Value));
break;
}
case EmptyResult:
context.Result = new JsonResult(new BaseResponse<object>(200, "请求成功"));
break;
case ContentResult:
{
var result = context.Result as ContentResult;
context.Result = new JsonResult(new BaseResponse<object>(200, "请求成功", result.Content));
break;
}
case OkResult:
{
context.Result = new JsonResult(new BaseResponse<object>(200, "请求成功"));
}
break;
}
}
//_logger.LogInformation("");
base.OnActionExecuted(context);
}
/// <summary>
/// 在Controller的Action执行前执行
/// </summary>
/// <param name="context"></param>
public override async void OnActionExecuting(ActionExecutingContext context)
{
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (actionDescriptor != null)
{
var methodInfo = actionDescriptor.MethodInfo;
var hasAllowAnonymous = methodInfo.GetCustomAttributes(true)
.OfType<AllowAnonymousAttribute>()
.Any();
//获取action名称
var actionName = actionDescriptor.ActionName;
//获取控制器名称
var controllerName = actionDescriptor.ControllerName;
//判断是否是刷新Token的请求
if (controllerName == "Login" && actionName == "RefreshToken")
{
//获取body参数对象中token
var requestBody = context.ActionArguments.Values.FirstOrDefault();
if (requestBody != null)
{
var token = context.HttpContext.Request.Headers["Authorization"].FirstOrDefault();
if (!string.IsNullOrEmpty(token))
{
token = token.Replace("Bearer ", "");
}
else
{
context.Result = new JsonResult(new BaseResponse<object>(1000, "刷新token需要请求头同时带上原token"));
return;
}
var parms = requestBody as RefreshTokenRequest;
if (parms.Token != token)
{
context.Result = new JsonResult(new BaseResponse<object>(1000, "参数token与头部token不一致"));
return;
}
if ((RefreshTokenExpToken(context, token)))
{
context.HttpContext.Response.StatusCode = 402;
context.Result = new JsonResult(new BaseResponse<object>(402, "账号被其他人登录,请重新登录"));
return;
}
}
}
if (!hasAllowAnonymous)
{
var request = context.HttpContext.Request;
var token = request.Headers["Authorization"].FirstOrDefault();
if (!string.IsNullOrEmpty(token))
{
token = token.Replace("Bearer ", "");
}
if (IsExpRedisToken(context, token))
{
context.HttpContext.Response.StatusCode = 402;
context.Result = new JsonResult(new BaseResponse<object>(402, "账号被其他人登录,请重新登录"));
return;
}
}
}
base.OnActionExecuting(context);
}
/// <summary>
/// 是否返回过期
/// </summary>
/// <param name="context"></param>
/// <param name="token"></param>
/// <returns></returns>
private bool IsExpRedisToken(ActionExecutingContext context, string token)
{
if (_nacosConfig.Namespace == "dev")
{
return false;
}
var userId = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value;
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId))
{
return true;
}
// 验证Token是否存在于Redis中
var redisToken = RedisHelper.Instance.HGet("Login_Token", userId);
if (string.IsNullOrEmpty(redisToken) || token != redisToken)
{
return true;
}
return false;
}
/// <summary>
/// 是否返回过期
/// </summary>
/// <param name="context"></param>
/// <param name="token"></param>
/// <returns></returns>
private bool RefreshTokenExpToken(ActionExecutingContext context, string token)
{
if (_nacosConfig.Namespace == "dev")
{
return false;
}
var userId = context.HttpContext.User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value;
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(userId))
{
return true;
}
// 验证Token是否存在于Redis中
var redisToken = RedisHelper.Instance.HGet("Login_Token", userId);
//
if (string.IsNullOrEmpty(redisToken) || token != redisToken)
{
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,20 @@

using Microsoft.AspNetCore.SignalR;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Yitter.IdGenerator;
using YuanXuan.IM.Common.Attributes;
namespace YuanXuan.IM.Api.Hangfire
{
[Inject]
public class HangfireJobs : IAdminHangfireJobs
{
private readonly ILogger<HangfireJobs> _logger;
public HangfireJobs(ILogger<HangfireJobs> logger)
{
_logger = logger;
}
}
}

View File

@ -0,0 +1,6 @@
namespace YuanXuan.IM.Api.Hangfire
{
public interface IAdminHangfireJobs
{
}
}

View File

@ -0,0 +1,26 @@
using Hangfire.Redis.StackExchange;
namespace YuanXuan.IM.Api.Hangfire
{
public class InitialHangfireService : IHostedService
{
private readonly IConfiguration _configuration;
public InitialHangfireService(IConfiguration configuration)
{
_configuration = configuration;
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public void Dispose() {
}
}
}

View File

@ -0,0 +1,14 @@
using Hangfire.Dashboard;
namespace YuanXuan.IM.Api.Hangfire
{
public class MyHangfireFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
return true; // 允许远程无限制访问
}
}
}

View File

@ -0,0 +1,28 @@
using Hangfire;
using Hangfire.Redis.StackExchange;
using YuanXuan.IM.Common.Configs;
namespace YuanXuan.IM.Api.Hangfire
{
public static class WskService
{
public static void AddWskService(this IServiceCollection services, IConfiguration configuration)
{
if (services==null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddHostedService<InitialHangfireService>();
var HangfireConfig = configuration.GetSection("Hangfire").Get<HangFireSettings>();
HangFireSettings.hangfireStorage = new RedisStorage(HangfireConfig.ConnectionString, new RedisStorageOptions
{
Db = HangfireConfig.Db,
FetchTimeout = TimeSpan.FromSeconds(30),
});
services.AddHangfire(config => config.UseStorage(HangFireSettings.hangfireStorage));
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Exceptions;
using YuanXuan.IM.Common.Helpers;
using YuanXuan.IM.Infrastructure.Redis;
namespace YuanXuan.IM.Api.Helper
{
/// <summary>
/// 手机跳H5用户签名帮助类
/// </summary>
public static class MobileToH5UserSign
{
/// <summary>
/// 获取用户跳转H5的签名并缓存sign 1小时
/// </summary>
/// <param name="UserId"></param>
/// <param name="DataId"></param>
/// <returns></returns>
public static string GetUserSign(string UserId, long? DataId)
{
//生成sign规则为Id + "&" + userId
var sign = $"{UserId}";
if (DataId != null)
{
sign = $"{DataId}&" + sign;
}
//aes加密sign
sign = AesEncryptHelper.EncryptToShortString(sign);
//sign进行base64
// 将字符串转换为字节数组
byte[] byteArray = Encoding.UTF8.GetBytes(sign);
// 将字节数组转换为Base64字符串
string base64String = Convert.ToBase64String(byteArray);
//存储到redis中有效期1小时
RedisHelper.Instance.Set(base64String, $"{DataId}&{UserId}", TimeSpan.FromHours(1));
return base64String;
}
/// <summary>
/// 解密用户跳转H5的签名获取Id有可能为null和userId
/// </summary>
/// <param name="sign"></param>
/// <param name="parCount">正确参数个数</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static (long? DataId, long UserId) DecryptUserSign(string sign, int parCount)
{
//判断redis中是否存在sign
var redisSign = RedisHelper.Instance.Get(sign);
if (string.IsNullOrEmpty(redisSign))
{
throw new BusinessException("链接已过期,请重新获取");
}
//sign的base64解码
byte[] decodedBytes = Convert.FromBase64String(sign);
// 转换为UTF8字符串
string decodedString = Encoding.UTF8.GetString(decodedBytes);
//解密sign
sign = AesEncryptHelper.DecryptFromShortString(decodedString);
var arr = sign.Split('&');
if (parCount!=arr.Length)
{
throw new BusinessException("参数错误");
}
if (arr.Length == 1)
{
var userId = Convert.ToInt64(arr[0]);
return (null, userId);
}
else if (true)
{
var id = Convert.ToInt64(arr[0]);
var userId = Convert.ToInt64(arr[1]);
return (id, userId);
}
else
{
throw new BusinessException("参数错误");
}
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using Yitter.IdGenerator;
using YuanXuan.IM.Infrastructure.Redis;
namespace YuanXuan.IM.Api.Helper
{
/// <summary>
/// IdWorker自动注册帮助类 需要先注册Redis
/// </summary>
public class WorkerIdAutoRegisterHelper
{
private static System.Timers.Timer _timer;
public static IdGeneratorOptions GetIdGeneratorOptions()
{
byte workerIdBitLength = 6;
var maxWorkId = Math.Pow(2, workerIdBitLength) - 1;
var workIdKey = "idgen:workid";
var workId = GetNextWorkId();
while (!RedisHelper.Instance.SetNx($"{workIdKey}:{workId}", 1))
{
// workId 已被占用获取下一个workId
workId = GetNextWorkId();
};
// 设置5分钟过期
RedisHelper.Instance.Expire($"{workIdKey}:{workId}", 60 * 5);
// 设置定时器每4分钟更新一次过期时间
SetTimer(4, (s, e) =>
{
RedisHelper.Instance.Expire($"{workIdKey}:{workId}", 60 * 5);
});
// WorkerIdBitLength + SeqBitLength 不超过 22
return new IdGeneratorOptions
{
WorkerIdBitLength = workerIdBitLength,
SeqBitLength = 6, // 数值越高性能越好但是Id也越长
WorkerId = (ushort)workId
};
long GetNextWorkId()
{
var workId = RedisHelper.Instance.IncrBy(workIdKey,1);
if (workId > maxWorkId)
{
// 大于了最大可用WorkId,重置workId并获取
RedisHelper.Instance.Set(workIdKey, 0);
workId = RedisHelper.Instance.IncrBy(workIdKey, 1);
}
Console.WriteLine($"================================================分配到的workId:{workId}===============================================================================");
return workId;
}
}
private static void SetTimer(int mins, ElapsedEventHandler eh)
{
// 创建一个 Timer 实例,并设置其相关属性
_timer = new System.Timers.Timer(TimeSpan.FromMinutes(mins).TotalMilliseconds); // 4 分钟
_timer.Elapsed += eh;
_timer.AutoReset = true; // 设置 Timer 实例能否多次触发
_timer.Enabled = true; // 启动 Timer 实例
}
}
}

150
YuanXuan.IM.Api/Program.cs Normal file
View File

@ -0,0 +1,150 @@
using Hangfire;
using Hangfire.Dashboard;
using LearningOfficer.OA.Mobile.Api.Filters;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System.Text;
using Yitter.IdGenerator;
using YuanXuan.IM.Api.CollectionExtensions;
using YuanXuan.IM.Api.Hangfire;
using YuanXuan.IM.Api.Helper;
using YuanXuan.IM.Api.Proxy;
namespace YuanXuan.IM.Api
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var Configuration = builder.Configuration;
var services = builder.Services;
//Services=============================
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddNacos(Configuration, builder.Host);
services.AddYarpWithNacos(Configuration);
services.AddPollyPolicies();
services.AddControllers().AddJsonOptions(option =>
{
option.JsonSerializerOptions.PropertyNamingPolicy = new CustomJsonNamingPolicy();
});
services.AddControllers(opt =>
{
opt.Filters.Add<GlobalExceptionCatchFilter>();
opt.Filters.Add<UniformResultActionFilter>();
opt.Filters.Add<GlobalOperationLogFilter>();
//// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƽ<EFBFBD><C6BD>ɵ<EFBFBD><C9B5><EFBFBD><E8B1B8>¼
//opt.Filters.Add<AuthonizationFilter>();
}).AddNewtonsoftJson(opt =>
{
//<2F><><EFBFBD><EFBFBD>ѭ<EFBFBD><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//<2F><><EFBFBD>ı<EFBFBD><C4B1>ֶδ<D6B6>С
opt.SerializerSettings.ContractResolver = new DefaultContractResolver();
opt.SerializerSettings.ContractResolver = new CustomContractResolver();
//<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ĭ<EFBFBD>ϸ<EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
opt.SerializerSettings.Converters.Add(new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" });
});
// <20><><EFBFBD>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
services.BatchRegisterServices();
services.AddApiVersion();
services.AddJwtAuth(Configuration);
services.AddSwagger();
services.AddHttpClient();
services.AddSerilog(Configuration, builder.Host, builder.Environment);
services.AddRedis(Configuration);
services.AddSqlSugar(Configuration, builder.Environment);
services.AddConfigureOptions(Configuration);
services.AddHttpContextAccessor();
services.AddWskService(Configuration);
services.AddHangfireServer();
YitIdHelper.SetIdGenerator(WorkerIdAutoRegisterHelper.GetIdGeneratorOptions());
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "YuanXuan-IM v1");
});
app.UseCors();
}
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
if (context.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase))
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, encoding: Encoding.UTF8
, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
{
var body = await reader.ReadToEndAsync();
context.Items.Add("body", body);
context.Request.Body.Position = 0;
}
}
if (context.Request.Method.Equals("Put", StringComparison.OrdinalIgnoreCase))
{
context.Request.EnableBuffering();
using (var reader = new StreamReader(context.Request.Body, encoding: Encoding.UTF8
, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
{
var body = await reader.ReadToEndAsync();
context.Items.Add("body", body);
context.Request.Body.Position = 0;
}
}
await next.Invoke();
});
app.UseHangfireDashboard("/hang", new DashboardOptions
{
IgnoreAntiforgeryToken = true,
DashboardTitle = "Hangfire<72><65><EFBFBD>",
Authorization = new[] { new MyHangfireFilter() },
IsReadOnlyFunc = (DashboardContext context) => false
});
var LocalTimeZone = new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Local
};
//RecurringJob.AddOrUpdate<IAdminHangfireJobs>("OverdueTasks", x => x.OverdueTasks(), "0 5 0 * * ?", LocalTimeZone);
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// 映射YARP反向代理
app.MapReverseProxy();
app.Run();
}
}
}

View File

@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34020",
"sslPort": 44342
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5070",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7045;http://localhost:5070",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,269 @@
using Nacos.V2;
using Newtonsoft.Json;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Configuration;
namespace YuanXuan.IM.Api.Proxy
{
/// <summary>
/// Nacos代理配置提供者
/// </summary>
public class NacosProxyConfigProvider : IProxyConfigProvider
{
private readonly INacosNamingService _nacosNamingService;
private readonly IConfiguration _configuration;
private volatile IProxyConfig _config;
private CancellationTokenSource _cts;
public NacosProxyConfigProvider(INacosNamingService nacosNamingService, IConfiguration configuration)
{
_nacosNamingService = nacosNamingService;
_configuration = configuration;
// 立即从Nacos获取服务实例
_config = CreateConfig();
// 通知YARP配置已变更
ProxyConfig.SignalChange();
_cts = new CancellationTokenSource();
// 启动后台任务定期从Nacos更新服务实例
Task.Run(async () => await UpdateConfigPeriodically(), _cts.Token);
}
/// <summary>
/// 创建配置
/// </summary>
/// <returns></returns>
private IProxyConfig CreateConfig()
{
var routes = new List<RouteConfig>
{
new RouteConfig
{
RouteId = "api_route",
ClusterId = "api_cluster",
Match = new RouteMatch
{
Path = "/api/{**catch-all}"
}
},
new RouteConfig
{
RouteId = "user_route",
ClusterId = "user_cluster",
Match = new RouteMatch
{
Path = "/user/{**catch-all}"
}
},
new RouteConfig
{
RouteId = "testdemo1",
ClusterId = "test_cluster",
Match = new RouteMatch
{
Path = "/test/{**catch-all}"
}
}
};
var clusters = new List<ClusterConfig>();
try
{
// 从Nacos获取YuanXuan.Api服务实例指定分组为qx-im
var apiServiceInstances = _nacosNamingService.GetAllInstances("YuanXuan.Api", "qx-im").Result;
if (apiServiceInstances.Any())
{
// 创建api_cluster集群配置
var apiDestinations = new Dictionary<string, DestinationConfig>();
foreach (var instance in apiServiceInstances)
{
if (instance.Healthy)
{
var destinationId = $"{instance.Ip}:{instance.Port}";
apiDestinations.Add(destinationId, new DestinationConfig
{
Address = $"http://{instance.Ip}:{instance.Port}"
});
}
}
clusters.Add(new ClusterConfig
{
ClusterId = "api_cluster",
Destinations = apiDestinations
});
}
// 从Nacos获取User.Service服务实例指定分组为qx-im
try
{
var userServiceInstances = _nacosNamingService.GetAllInstances("User.Service", "qx-im").Result;
if (userServiceInstances.Any())
{
// 创建user_cluster集群配置
var userDestinations = new Dictionary<string, DestinationConfig>();
foreach (var instance in userServiceInstances)
{
if (instance.Healthy)
{
var destinationId = $"{instance.Ip}:{instance.Port}";
userDestinations.Add(destinationId, new DestinationConfig
{
Address = $"http://{instance.Ip}:{instance.Port}"
});
}
}
clusters.Add(new ClusterConfig
{
ClusterId = "user_cluster",
Destinations = userDestinations
});
}
}
catch (Exception ex)
{
Console.WriteLine($"从Nacos获取User.Service服务实例失败: {ex.Message}");
}
// 从Nacos获取Test.Service服务实例指定分组为qx-im
try
{
var testServiceInstances = _nacosNamingService.GetAllInstances("Test.Service", "qx-im").Result;
if (testServiceInstances.Any())
{
// 创建test_cluster集群配置
var testDestinations = new Dictionary<string, DestinationConfig>();
foreach (var instance in testServiceInstances)
{
if (instance.Healthy)
{
var destinationId = $"{instance.Ip}:{instance.Port}";
testDestinations.Add(destinationId, new DestinationConfig
{
Address = $"http://{instance.Ip}:{instance.Port}"
});
}
}
clusters.Add(new ClusterConfig
{
ClusterId = "test_cluster",
Destinations = testDestinations
});
}
}
catch (Exception ex)
{
Console.WriteLine($"从Nacos获取Test.Service服务实例失败: {ex.Message}");
}
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}" + JsonConvert.SerializeObject(clusters));
}
catch (Exception ex)
{
Console.WriteLine($"从Nacos获取服务实例失败: {ex.Message}");
}
// 如果没有从Nacos获取到api_cluster集群使用默认配置
if (!clusters.Any(c => c.ClusterId == "api_cluster"))
{
clusters.Add(new ClusterConfig
{
ClusterId = "api_cluster",
Destinations = new Dictionary<string, DestinationConfig>
{
{
"default", new DestinationConfig { Address = "http://localhost:5001" }
}
}
});
}
// 如果没有从Nacos获取到user_cluster集群使用默认配置
if (!clusters.Any(c => c.ClusterId == "user_cluster"))
{
clusters.Add(new ClusterConfig
{
ClusterId = "user_cluster",
Destinations = new Dictionary<string, DestinationConfig>
{
{
"default", new DestinationConfig { Address = "http://localhost:5181" }
}
}
});
}
// 如果没有从Nacos获取到test_cluster集群使用默认配置
if (!clusters.Any(c => c.ClusterId == "test_cluster"))
{
clusters.Add(new ClusterConfig
{
ClusterId = "test_cluster",
Destinations = new Dictionary<string, DestinationConfig>
{
{
"default", new DestinationConfig { Address = "http://localhost:5252" }
}
}
});
}
return new ProxyConfig(routes, clusters, DateTime.UtcNow);
}
/// <summary>
/// 获取代理配置
/// </summary>
/// <returns></returns>
public IProxyConfig GetConfig() => _config;
/// <summary>
/// 定期更新配置
/// </summary>
/// <returns></returns>
private async Task UpdateConfigPeriodically()
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(30), _cts.Token); // 每30秒更新一次
await UpdateConfig();
}
catch (TaskCanceledException)
{
// 任务被取消,退出循环
break;
}
catch (Exception ex)
{
Console.WriteLine($"更新Nacos代理配置失败: {ex.Message}");
}
}
}
/// <summary>
/// 更新配置
/// </summary>
/// <returns></returns>
private async Task UpdateConfig()
{
// 重新从Nacos获取服务实例并更新配置
_config = CreateConfig();
// 通知YARP配置已变更
ProxyConfig.SignalChange();
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Extensions.Primitives;
using System.Threading;
using Yarp.ReverseProxy.Configuration;
namespace YuanXuan.IM.Api.Proxy
{
/// <summary>
/// 代理配置
/// </summary>
public class ProxyConfig : IProxyConfig
{
private static CancellationTokenSource _cts = new CancellationTokenSource();
public ProxyConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, DateTime timestamp)
{
Routes = routes;
Clusters = clusters;
Timestamp = timestamp;
}
public IReadOnlyList<RouteConfig> Routes { get; }
public IReadOnlyList<ClusterConfig> Clusters { get; }
public DateTime Timestamp { get; }
public IChangeToken ChangeToken => new CancellationChangeToken(_cts.Token);
/// <summary>
/// 通知配置变更
/// </summary>
public static void SignalChange()
{
var oldCts = Interlocked.Exchange(ref _cts, new CancellationTokenSource());
oldCts.Cancel();
}
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="1\**" />
<Content Remove="1\**" />
<EmbeddedResource Remove="1\**" />
<None Remove="1\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Polly" Version="8.6.5" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YuanXuan.IM.Common\YuanXuan.IM.Common.csproj" />
<ProjectReference Include="..\YuanXuan.IM.Core\YuanXuan.IM.Core.csproj" />
<ProjectReference Include="..\YuanXuan.IM.Infrastructure\YuanXuan.IM.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.22" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@YuanXuan.IM.Api_HostAddress = http://localhost:5070
GET {{YuanXuan.IM.Api_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,28 @@
{
"nacos": {
"Namespace": "public", //Id
"ServerAddresses": [ "http://192.168.2.9:8848" ], //Nacos
"UserName": "nacos", // Nacos
"Password": "qwe123!@#", // Nacos
"Group": "qx-im", //
"Listeners": [ //
{
"Optional": false,
"DataId": "qx-im-api-dev", //
"Group": "qx-im" //
}
],
"ServiceName": "YuanXuan.Api", //
// Ncaos
"DefaultTimeOut": 15000,
"ListenInterval": 1000,
"GroupName": "qx-im",
"RegisterEnabled": true,
"InstanceEnabled": true,
"Ephemeral": true,
"ConfigUseRpc": true,
"NamingUseRpc": true,
"LBStrategy": "WeightRandom" //WeightRandom WeightRoundRobin
}
}

View File

@ -0,0 +1,21 @@
{
"nacos": {
"Listeners": [ //
{
"Optional": false,
"DataId": "qx-im-api-dev", //
"Group": "qx-im" //
}
],
"DefaultTimeOut": 15,
"ListenInterval": 1000,
"ServiceName": "qx-im-api-dev", //
"Namespace": "public", //Id
"ServerAddresses": [ "http://192.168.2.9:8848" ], //Nacos
"UserName": "nacos",
"Password": "qwe123!@#",
"ConfigUseRpc": false, //false-httptrue-grpc
"NamingUseRpc": false //false-httptrue-grpc
}
}

View File

@ -0,0 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
namespace YuanXuan.IM.Common.Attributes
{
[AttributeUsage(AttributeTargets.Class)]
public class InjectAttribute : Attribute
{
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Scoped;
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Configs
{
public class ConnectionStringsSettings
{
public string Redis { get; set; }
public string Db { get; set; }
public string UserCenterDb { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Hangfire;
namespace YuanXuan.IM.Common.Configs
{
public class HangFireSettings
{
public string ConnectionString { get; set; }
public int Db { get; set; } = 0;
public string cron { get; set; }
public static JobStorage hangfireStorage { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Configs
{
public class ImConfig
{
public int SDKAppID { get; set; }
public string Key { get; set; }
public int ExpiredTime { get; set; }
/// <summary>
/// 头像默认地址
/// </summary>
public string DefaultAvater { get; set; }
/// <summary>
/// 管理员用户
/// </summary>
public string AdminUserId { get; set; }
/// <summary>
/// api头部地址
/// </summary>
public string ApiHeadUrl { get; set; }
/// <summary>
/// 视频通话状态
/// </summary>
public bool VideoCall { get; set; }
/// <summary>
/// 语音通话状态
/// </summary>
public bool VoiceCall { get; set; }
}
public class ImConfigResult
{
public int SDKAppID { get; set; }
/// <summary>
/// 视频通话状态
/// </summary>
public bool VideoCall { get; set; }
/// <summary>
/// 语音通话状态
/// </summary>
public bool VoiceCall { get; set; }
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Configs
{
public class JwtSettings
{
/// <summary>
/// AccessToken密钥
/// </summary>
public string AccessSecret { get; set; } = string.Empty;
/// <summary>
/// 签发人
/// </summary>
public string Issuer { get; set; } = string.Empty;
/// <summary>
/// 受众
/// </summary>
public string Audience { get; set; } = string.Empty;
/// <summary>
/// AccessToken有效时长
/// </summary>
public int AccessExpiration { get; set; }
/// <summary>
/// RefreshExpiration有效时长
/// </summary>
public int RefreshExpiration { get; set; }
/// <summary>
/// 允许的时差
/// </summary>
public int ClockSkew { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Configs
{
public class NacosConfig
{
/// <summary>
/// Nacos命名空间
/// </summary>
public string Namespace { get; set; }
}
}

View File

@ -0,0 +1,72 @@
using RabbitMQ.Client;
namespace YuanXuan.IM.Common.Configs
{
/// <summary>
/// RabbitMQ 配置
/// </summary>
public class RabbitMQConfig
{
/// <summary>
/// RabbitMQ 服务器地址
/// </summary>
public string HostName { get; set; }
/// <summary>
/// 端口号
/// </summary>
public int Port { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
/// <summary>
/// 虚拟主机
/// </summary>
public string VirtualHost { get; set; }
/// <summary>
/// im推送mq配置
/// </summary>
public BaseMqBusinessConfig imMq { get; set; }
}
/// <summary>
/// 对应业务的mq配置
/// </summary>
public class BaseMqBusinessConfig
{
/// <summary>
/// 队列名称
/// </summary>
public string QueueName { get; set; }
/// <summary>
/// 交换机名称
/// </summary>
public string ExchangeName { get; set; }
/// <summary>
/// 路由键
/// </summary>
public string RoutingKey { get; set; }
/// <summary>
/// 是否启用消息持久化
/// </summary>
public bool Durable { get; set; } = true;
/// <summary>
/// 是否自动删除队列
/// </summary>
public bool AutoDelete { get; set; } = false;
/// <summary>
/// 预取计数
/// </summary>
public ushort PrefetchCount { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Configs
{
public class UpAppVersionConfig
{
/// <summary>
/// IOS链接
/// </summary>
public string IOSUrl { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.ALiYun
{
public class CodeMsg
{
public int Code { get; set; }
public string Data { get; set; }
public string Reson { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.ALiYun
{
public class OSSConfigResult
{
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string Endpoint { get; set; }
public string BucketName { get; set; }
public long Size { get; set; }
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.Consul
{
public class ConsulServiceInstance
{
/// <summary>
/// 服务ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 服务名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 服务地址
/// </summary>
public string Address { get; set; }
/// <summary>
/// 服务端口
/// </summary>
public int Port { get; set; }
/// <summary>
/// 是否健康
/// </summary>
public bool IsHealthy { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.LoginMobile
{
public class LoginRequest
{
/// <summary>
/// 用户名
/// </summary>
public string username { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Pwd { get; set; }
/// <summary>
/// 登录类型 1:Android2Ios3扫码4H5
/// </summary>
public int loginType { get; set; } = 1;
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.LoginMobile
{
public class MobileTokenInfo
{
/// <summary>
/// 用户Id
/// </summary>
public string UserId { get; set; }
/// <summary>
/// 用户名称
/// </summary>
public string UserName { get; set; }
/// <summary>
/// Jwt_id
/// </summary>
public string Jwt_id { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Dtos.LoginMobile
{
public class RefreshTokenRequest
{
/// <summary>
/// 过期的token
/// </summary>
public string Token { get; set; }
/// <summary>
/// 刷新token
/// </summary>
public string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Entities
{
/// <summary>
/// 基础数据库实体类
/// </summary>
public class BaseEntity
{
[SugarColumn(IsPrimaryKey = true)]
public long Id { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Enums
{
/// <summary>
/// 二维码用途类型
/// </summary>
public enum QRCodeTypeEnum
{
/// <summary>
/// 登录到PC
/// </summary>
LoginPC = 1,
/// <summary>
/// 登录到统计
/// </summary>
LoginStatistic =2
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Enums
{
/// <summary>
/// 系统角色枚举
/// </summary>
public enum SysRoleEnum
{
/// <summary>
/// 超级管理员
/// </summary>
[Description("超级管理员")]
SuperAdmin = 1,
/// <summary>
/// 云校管理员
/// </summary>
[Description("云校管理员")]
CloudSchoolAdmin = 2,
/// <summary>
/// 运营管理员
/// </summary>
OperationAdmin = 3,
/// <summary>
/// 总部长
/// </summary>
[Description("总部长")]
GeneralMinisterAdmin = 1000,
/// <summary>
/// 部长
/// </summary>
[Description("部长")]
MinisterAdmin = 1001,
/// <summary>
/// 组长
/// </summary>
[Description("组长")]
TeamLeader = 1002,
/// <summary>
/// 学习官
/// </summary>
[Description("学习官")]
LearningOfficer = 1003,
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Exceptions
{
/// <summary>
/// 业务异常
/// </summary>
public class BusinessException : Exception
{
public BusinessExceptionCode ErrorCode { get; private set; }
public object BussinessExceptionData { get; private set; }
public BusinessException(string message, object friendlyData = null, BusinessExceptionCode errorCode = BusinessExceptionCode.BussinessError) : base(message)
{
ErrorCode = errorCode;
BussinessExceptionData = friendlyData;
}
}
/// <summary>
/// 业务异常码
/// </summary>
public enum BusinessExceptionCode
{
/// <summary>
/// 业务异常码
/// </summary>
BussinessError = 1000,
/// <summary>
/// 程序异常码
/// </summary>
AppError = 500,
/// <summary>
/// 访问令牌异常码
/// </summary>
AccessTokenError = 401,
/// <summary>
/// 访问令牌被顶号异常码
/// </summary>
AccessTokenTopNumberError = 402,
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Helpers
{
public static class AesEncryptHelper
{
static string key = "ThisIsAVerySecre";
static string iv = "16ByteIV12345678";
// 使用CBC模式加密并返回Base64字符串
public static string EncryptToShortString(string plainText)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Encoding.UTF8.GetBytes(key);
aesAlg.IV = Encoding.UTF8.GetBytes(iv);
aesAlg.Mode = CipherMode.CBC;
aesAlg.Padding = PaddingMode.PKCS7;
ICryptoTransform encryptor = aesAlg.CreateEncryptor();
byte[] encryptedBytes = encryptor.TransformFinalBlock(
Encoding.UTF8.GetBytes(plainText), 0, plainText.Length);
return Convert.ToBase64String(encryptedBytes);
}
}
// 解密Base64加密字符串
public static string DecryptFromShortString(string cipherText)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Encoding.UTF8.GetBytes(key);
aesAlg.IV = Encoding.UTF8.GetBytes(iv);
aesAlg.Mode = CipherMode.CBC;
aesAlg.Padding = PaddingMode.PKCS7;
ICryptoTransform decryptor = aesAlg.CreateDecryptor();
byte[] cipherBytes = Convert.FromBase64String(cipherText);
byte[] decryptedBytes = decryptor.TransformFinalBlock(
cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
}

View File

@ -0,0 +1,52 @@
using Aliyun.OSS;
using Aliyun.OSS.Common;
using Castle.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Attributes;
using YuanXuan.IM.Common.Dtos.ALiYun;
namespace YuanXuan.IM.Common.Helpers
{
[Inject(Lifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient)]
public class AliyunOssHelper
{
private readonly IOptionsMonitor<OSSConfigResult> ossConfig;
private readonly ILogger<AliyunOssHelper> logger;
public AliyunOssHelper(IOptionsMonitor<OSSConfigResult> ossConfig,
ILogger<AliyunOssHelper> logger)
{
this.ossConfig = ossConfig;
this.logger = logger;
}
public string UploadByStream(Stream stream, string fileName)
{
try
{
var client = new OssClient(ossConfig.CurrentValue.Endpoint, ossConfig.CurrentValue.AccessKeyId, ossConfig.CurrentValue.AccessKeySecret, new ClientConfiguration()
{
SignatureVersion = SignatureVersion.V4
});
client.SetRegion("cn-chengdu");
client.PutObject(ossConfig.CurrentValue.BucketName, fileName, stream);
return $"https://{ossConfig.CurrentValue.BucketName}.{ossConfig.CurrentValue.Endpoint}/{fileName.TrimStart('/')}";
}
catch (Exception ex)
{
logger.LogError("AliyunOssHelper UploadByStream Error:{0}", ex.Message);
}
return string.Empty;
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Helpers
{
/// <summary>
/// 异常通知
/// </summary>
public class ExceptionNotice
{
private static HttpClient httpClient = new HttpClient()
{
BaseAddress = new Uri("https://oapi.dingtalk.com/robot/send?access_token=339d1f43d3b2a084abaa77871ddd187b613206149962d844adf37a46a14359a1"),
};
/// <summary>
/// 发送异常信息
/// </summary>
/// <param name="exp">异常</param>
/// <param name="expSrc">异常来源(用于显示)</param>
/// <returns></returns>
public static async Task<bool> SendAsync(Exception exp, string expSrc)
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
if (env == "Development")
{
Console.WriteLine("*************** Excpetion ***************");
Console.WriteLine(exp.Message, exp);
Console.WriteLine("*************** Excpetion ***************");
return true;
}
var reponse = await httpClient.PostAsync(string.Empty, JsonContent.Create(new
{
msgtype = "markdown",
markdown = new
{
title = "Mobile.API抛出异常",
text = $"Mobile.API异常.描述:{exp.Message}\n详情:{exp}"
},
}));
return reponse.IsSuccessStatusCode;
}
}
}

View File

@ -0,0 +1,133 @@
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Configs;
using Microsoft.Extensions.Configuration;
using System.Reflection;
using System.IO;
namespace YuanXuan.IM.Common.Helpers
{
public class JwtHelper
{
private static JwtSettings _jwtSettings;
static JwtHelper()
{
// 初始化JWT配置
var configuration = new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
.AddJsonFile("appsettings.json", optional: true)
.AddJsonFile("appsettings.Development.json", optional: true)
.Build();
_jwtSettings = configuration.GetSection("Jwt").Get<JwtSettings>();
}
/// <summary>
/// 生成token
/// </summary>
/// <param name="uid"></param>
/// <param name="secretKey"></param>
/// <param name="issuer"></param>
/// <param name="audience"></param>
/// <param name="expires"></param>
/// <param name="claims"></param>
/// <returns></returns>
public static string CreateToken(string uid, string secretKey, string issuer, string audience, double expires, List<Claim> claims = null)
{
if (claims.IsNullOrEmpty())
claims = new();
claims.AddRange(new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, uid),
});
// 2. 从 appsettings.json 中读取SecretKey
var secret = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
// 3. 选择加密算法
var algorithm = SecurityAlgorithms.HmacSha256;
// 4. 生成Credentials
var signingCredentials = new SigningCredentials(secret, algorithm);
// 5. 根据以上生成token
var jwtSecurityToken = new JwtSecurityToken(
issuer, //Issuer
audience, //Audience
claims, //Claims,
DateTime.Now, //notBefore
DateTime.Now.AddSeconds(expires), //expires
signingCredentials //Credentials
);
// 6. 将token变为string
var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return token;
}
/// <summary>
/// 生成JWT token
/// </summary>
/// <param name="userId"></param>
/// <param name="userName"></param>
/// <returns></returns>
public static string GenerateToken(string userId, string userName)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Name, userName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
return CreateToken(userId, _jwtSettings.AccessSecret, _jwtSettings.Issuer, _jwtSettings.Audience, _jwtSettings.AccessExpiration);
}
/// <summary>
/// 生成刷新token
/// </summary>
/// <returns></returns>
public static string GenerateRefreshToken()
{
return Guid.NewGuid().ToString("N");
}
/// <summary>
/// 从过期的token中获取principal
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.AccessSecret)),
ValidateLifetime = false // 不验证过期时间
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
{
throw new SecurityTokenException("无效的token");
}
return principal;
}
}
}

View File

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Helpers
{
/// <summary>
/// MD5帮助类
/// </summary>
public static class MD5EncryptionHelper
{
/// <summary>
/// MD5字符串加密
/// </summary>
/// <param name="txt"></param>
/// <returns>加密后字符串</returns>
public static string MD5Encryption(string txt)
{
using (MD5 mi = MD5.Create())
{
byte[] buffer = Encoding.Default.GetBytes(txt);
//开始加密
byte[] newBuffer = mi.ComputeHash(buffer);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < newBuffer.Length; i++)
{
sb.Append(newBuffer[i].ToString("x2"));
}
return sb.ToString().ToUpper(); ;
}
}
}
}

View File

@ -0,0 +1,39 @@
using QRCoder;
namespace YuanXuan.IM.Common.Helpers
{
/// <summary>
/// 二维码生成器
/// </summary>
public class QRCoderHelper
{
/// <summary>
/// 生成二维码的 URL
/// </summary>
/// <param name="filePath">网络路径</param>
/// <returns>二维码图片的base64字符串</returns>
public static string GenerateQRCodeUrl(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentException("文件路径不能为空", nameof(filePath));
}
using (var qrGenerator = new QRCodeGenerator())
{
// 生成二维码数据
var qrCodeData = qrGenerator.CreateQrCode(filePath, QRCodeGenerator.ECCLevel.Q);
// 使用 PngByteQRCode 生成二维码字节数组
var pngByteQRCode = new PngByteQRCode(qrCodeData);
var qrCodeBytes = pngByteQRCode.GetGraphic(20);
// 将字节数组转换为 Base64 字符串
var base64String = Convert.ToBase64String(qrCodeBytes);
return $"data:image/png;base64,{base64String}";
}
}
}
}

View File

@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestSharp;
namespace YuanXuan.IM.Common.Helpers
{
/// <summary>
/// RestSharpHelper
/// </summary>
public class RestSharpHelper
{
private readonly RestClient _client;
/// <summary>
/// 构造函数,初始化 RestClient
/// </summary>
public RestSharpHelper()
{
_client = new RestClient();
}
/// <summary>
/// 发送 GET 请求
/// </summary>
/// <param name="url">完整的请求 URL</param>
/// <param name="headers">请求头</param>
/// <returns>响应数据</returns>
public async Task<string> GetAsync(string url, Dictionary<string, string>? headers = null)
{
var request = new RestRequest(url, Method.Get);
AddHeaders(request, headers);
var response = await _client.ExecuteAsync(request);
return HandleResponse(response);
}
/// <summary>
/// 发送 POST 请求
/// </summary>
/// <param name="url">完整的请求 URL</param>
/// <param name="body">请求体</param>
/// <param name="headers">请求头</param>
/// <returns>响应数据</returns>
public async Task<string> PostAsync(string url, object body, Dictionary<string, string>? headers = null)
{
var request = new RestRequest(url, Method.Post);
AddHeaders(request, headers);
request.AddBody(body);
var response = await _client.ExecuteAsync(request);
return HandleResponse(response);
}
/// <summary>
/// 发送 POST 请求
/// </summary>
/// <param name="url">完整的请求 URL</param>
/// <param name="body">请求体</param>
/// <param name="headers">请求头</param>
/// <returns>响应数据</returns>
public async Task<string> PostByJsonBodyAsync(string url, object body, Dictionary<string, string>? headers = null)
{
var request = new RestRequest(url, Method.Post);
AddHeaders(request, headers);
request.AddJsonBody(body);
var response = await _client.ExecuteAsync(request);
return HandleResponse(response);
}
/// <summary>
/// 发送 PUT 请求
/// </summary>
/// <param name="url">完整的请求 URL</param>
/// <param name="body">请求体</param>
/// <param name="headers">请求头</param>
/// <returns>响应数据</returns>
public async Task<string> PutAsync(string url, object body, Dictionary<string, string>? headers = null)
{
var request = new RestRequest(url, Method.Put);
AddHeaders(request, headers);
request.AddJsonBody(body);
var response = await _client.ExecuteAsync(request);
return HandleResponse(response);
}
/// <summary>
/// 添加请求头
/// </summary>
/// <param name="request">RestSharp 请求对象</param>
/// <param name="headers">请求头</param>
private void AddHeaders(RestRequest request, Dictionary<string, string>? headers)
{
if (headers != null)
{
foreach (var header in headers)
{
request.AddHeader(header.Key, header.Value);
}
}
}
/// <summary>
/// 处理响应
/// </summary>
/// <param name="response">RestSharp 响应对象</param>
/// <returns>响应数据</returns>
/// <exception cref="Exception">当响应失败时抛出异常</exception>
private string HandleResponse(RestResponse response)
{
if (response.IsSuccessful && response.Content != null)
{
return response.Content;
}
throw new Exception($"请求失败: {response.StatusCode}, 错误信息: {response.ErrorMessage ?? response.Content}");
}
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using ComponentAce.Compression.Libs.zlib;
namespace YuanXuan.IM.Common.Helpers
{
public class TLSSigAPIv2
{
private readonly int sdkappid;
private readonly string key;
public TLSSigAPIv2(int sdkappid, string key)
{
this.sdkappid = sdkappid;
this.key = key;
}
/**
* TRTC IM 使 UserSig
*
*
* userid - id32a-zA-Z0-9线
* expire - UserSig 86400 UserSig 使
*/
public string genUserSig(string userid, int expire = 180 * 86400)
{
return genUserSig(userid, expire, null, false);
}
/**
*
* TRTC PrivateMapKey
* PrivateMapKey UserSig 使 PrivateMapKey UserSig
* - UserSig UserID 使 TRTC UserSig UserID
* - PrivateMapKey UserID
* PrivateMapKey =>=>
*
*
* userid - id32a-zA-Z0-9线
* roomid - userid
* expire - PrivateMapKey 86400 PrivateMapKey 使
* privilegeMap - 使 8
* - 1 0000 0001 = 1
* - 2 0000 0010 = 2
* - 3 0000 0100 = 4
* - 4 0000 1000 = 8
* - 5 0001 0000 = 16
* - 6 0010 0000 = 32
* - 7 0100 0000 = 64
* - 8 1000 0000 = 200
* - privilegeMap == 1111 1111 == 255 userid roomid
* - privilegeMap == 0010 1010 == 42 userid
*/
public string genPrivateMapKey(string userid, int expire, uint roomid, uint privilegeMap)
{
byte[] userbuf = genUserBuf(userid, roomid, expire, privilegeMap, 0, "");
System.Console.WriteLine(userbuf);
return genUserSig(userid, expire, userbuf, true);
}
/**
*
* TRTC PrivateMapKey
* PrivateMapKey UserSig 使 PrivateMapKey UserSig
* - UserSig UserID 使 TRTC UserSig UserID
* - PrivateMapKey UserID
* PrivateMapKey =>=>
*
*
* userid - id32a-zA-Z0-9线
* roomstr - userid
* expire - PrivateMapKey 86400 PrivateMapKey 使
* privilegeMap - 使 8
* - 1 0000 0001 = 1
* - 2 0000 0010 = 2
* - 3 0000 0100 = 4
* - 4 0000 1000 = 8
* - 5 0001 0000 = 16
* - 6 0010 0000 = 32
* - 7 0100 0000 = 64
* - 8 1000 0000 = 200
* - privilegeMap == 1111 1111 == 255 userid roomid
* - privilegeMap == 0010 1010 == 42 userid
*/
public string genPrivateMapKeyWithStringRoomID(string userid, int expire, string roomstr, uint privilegeMap)
{
byte[] userbuf = genUserBuf(userid, 0, expire, privilegeMap, 0, roomstr);
System.Console.WriteLine(userbuf);
return genUserSig(userid, expire, userbuf, true);
}
private string genUserSig(string userid, int expire, byte[] userbuf, bool userBufEnabled)
{
DateTime epoch = new DateTime(1970, 1, 1); // unix 时间戳
Int64 currTime = (Int64)(DateTime.UtcNow - epoch).TotalMilliseconds / 1000;
string base64UserBuf;
string jsonData;
if (true == userBufEnabled)
{
base64UserBuf = Convert.ToBase64String(userbuf);
string base64sig = HMACSHA256(userid, currTime, expire, base64UserBuf, userBufEnabled);
// 没有引入 json 库,所以这里手动进行组装
jsonData = String.Format("{{"
+ "\"TLS.ver\":" + "\"2.0\","
+ "\"TLS.identifier\":" + "\"{0}\","
+ "\"TLS.sdkappid\":" + "{1},"
+ "\"TLS.expire\":" + "{2},"
+ "\"TLS.time\":" + "{3},"
+ "\"TLS.sig\":" + "\"{4}\","
+ "\"TLS.userbuf\":" + "\"{5}\""
+ "}}", userid, sdkappid, expire, currTime, base64sig, base64UserBuf);
}
else
{
// 没有引入 json 库,所以这里手动进行组装
string base64sig = HMACSHA256(userid, currTime, expire, "", false);
jsonData = String.Format("{{"
+ "\"TLS.ver\":" + "\"2.0\","
+ "\"TLS.identifier\":" + "\"{0}\","
+ "\"TLS.sdkappid\":" + "{1},"
+ "\"TLS.expire\":" + "{2},"
+ "\"TLS.time\":" + "{3},"
+ "\"TLS.sig\":" + "\"{4}\""
+ "}}", userid, sdkappid, expire, currTime, base64sig);
}
byte[] buffer = Encoding.UTF8.GetBytes(jsonData);
return Convert.ToBase64String(CompressBytes(buffer))
.Replace('+', '*').Replace('/', '-').Replace('=', '_');
}
public byte[] genUserBuf(string account, uint dwAuthID, int dwExpTime, uint dwPrivilegeMap, uint dwAccountType, string roomStr)
{
int length = 1 + 2 + account.Length + 20;
int offset = 0;
if (roomStr.Length > 0)
length = length + 2 + roomStr.Length;
byte[] userBuf = new byte[length];
if (roomStr.Length > 0)
userBuf[offset++] = 1;
else
userBuf[offset++] = 0;
userBuf[offset++] = (byte)((account.Length & 0xFF00) >> 8);
userBuf[offset++] = (byte)(account.Length & 0x00FF);
byte[] accountByte = System.Text.Encoding.UTF8.GetBytes(account);
accountByte.CopyTo(userBuf, offset);
offset += account.Length;
//dwSdkAppid
userBuf[offset++] = (byte)((sdkappid & 0xFF000000) >> 24);
userBuf[offset++] = (byte)((sdkappid & 0x00FF0000) >> 16);
userBuf[offset++] = (byte)((sdkappid & 0x0000FF00) >> 8);
userBuf[offset++] = (byte)(sdkappid & 0x000000FF);
//dwAuthId
userBuf[offset++] = (byte)((dwAuthID & 0xFF000000) >> 24);
userBuf[offset++] = (byte)((dwAuthID & 0x00FF0000) >> 16);
userBuf[offset++] = (byte)((dwAuthID & 0x0000FF00) >> 8);
userBuf[offset++] = (byte)(dwAuthID & 0x000000FF);
//time_t now = time(0);
//uint32_t expire = now + dwExpTime;
long expire = dwExpTime + (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
userBuf[offset++] = (byte)((expire & 0xFF000000) >> 24);
userBuf[offset++] = (byte)((expire & 0x00FF0000) >> 16);
userBuf[offset++] = (byte)((expire & 0x0000FF00) >> 8);
userBuf[offset++] = (byte)(expire & 0x000000FF);
//dwPrivilegeMap
userBuf[offset++] = (byte)((dwPrivilegeMap & 0xFF000000) >> 24);
userBuf[offset++] = (byte)((dwPrivilegeMap & 0x00FF0000) >> 16);
userBuf[offset++] = (byte)((dwPrivilegeMap & 0x0000FF00) >> 8);
userBuf[offset++] = (byte)(dwPrivilegeMap & 0x000000FF);
//dwAccountType
userBuf[offset++] = (byte)((dwAccountType & 0xFF000000) >> 24);
userBuf[offset++] = (byte)((dwAccountType & 0x00FF0000) >> 16);
userBuf[offset++] = (byte)((dwAccountType & 0x0000FF00) >> 8);
userBuf[offset++] = (byte)(dwAccountType & 0x000000FF);
if (roomStr.Length > 0)
{
userBuf[offset++] = (byte)((roomStr.Length & 0xFF00) >> 8);
userBuf[offset++] = (byte)(roomStr.Length & 0x00FF);
byte[] roomStrByte = System.Text.Encoding.UTF8.GetBytes(roomStr);
roomStrByte.CopyTo(userBuf, offset);
offset += roomStr.Length;
}
return userBuf;
}
private static byte[] CompressBytes(byte[] sourceByte)
{
MemoryStream inputStream = new MemoryStream(sourceByte);
Stream outStream = CompressStream(inputStream);
byte[] outPutByteArray = new byte[outStream.Length];
outStream.Position = 0;
outStream.Read(outPutByteArray, 0, outPutByteArray.Length);
return outPutByteArray;
}
private static Stream CompressStream(Stream sourceStream)
{
MemoryStream streamOut = new MemoryStream();
ZOutputStream streamZOut = new ZOutputStream(streamOut, zlibConst.Z_DEFAULT_COMPRESSION);
CopyStream(sourceStream, streamZOut);
streamZOut.finish();
return streamOut;
}
public static void CopyStream(System.IO.Stream input, System.IO.Stream output)
{
byte[] buffer = new byte[2000];
int len;
while ((len = input.Read(buffer, 0, 2000)) > 0)
{
output.Write(buffer, 0, len);
}
output.Flush();
}
private string HMACSHA256(string identifier, long currTime, int expire, string base64UserBuf, bool userBufEnabled)
{
string rawContentToBeSigned = "TLS.identifier:" + identifier + "\n"
+ "TLS.sdkappid:" + sdkappid + "\n"
+ "TLS.time:" + currTime + "\n"
+ "TLS.expire:" + expire + "\n";
if (true == userBufEnabled)
{
rawContentToBeSigned += "TLS.userbuf:" + base64UserBuf + "\n";
}
using (HMACSHA256 hmac = new HMACSHA256())
{
UTF8Encoding encoding = new UTF8Encoding();
Byte[] textBytes = encoding.GetBytes(rawContentToBeSigned);
Byte[] keyBytes = encoding.GetBytes(key);
Byte[] hashBytes;
using (HMACSHA256 hash = new HMACSHA256(keyBytes))
hashBytes = hash.ComputeHash(textBytes);
return Convert.ToBase64String(hashBytes);
}
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Request
{
/// <summary>
/// 分页请求实体类
/// </summary>
public class PageRequest
{
/// <summary>
/// 当前页
/// </summary>
public int PageIndex { get; set; } = 1;
/// <summary>
/// 一页条数
/// </summary>
public int PageSize { get; set; } = 10;
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Response
{
public class BaseResponse
{
/// <summary>
/// 业务结果代码
/// </summary>
public int code { get; set; }
/// <summary>
/// 返回消息
/// </summary>
public string msg { get; set; } = "";
}
/// <summary>
/// 基础返回实体类
/// </summary>
/// <typeparam name="T"></typeparam>
public class BaseResponse<T> : BaseResponse
{
public BaseResponse(int code, string msg = "", T data = default)
{
this.code = code;
this.msg = msg;
this.data = data;
}
/// <summary>
/// 返回实体
/// </summary>
public T data { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Common.Request
{
/// <summary>
/// 分页响应实体类
/// </summary>
/// <typeparam name="T"></typeparam>
public class PageResponse<T>
{
/// <summary>
/// 总记录条数
/// </summary>
public int Total { get; set; }
/// <summary>
/// 响应数据
/// </summary>
public List<T> Items { get; set; }
}
}

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="aliyun-net-sdk-core" Version="1.6.2" />
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageReference Include="ComponentAce.Compression.Libs.zlib" Version="1.0.4" />
<PackageReference Include="Hangfire" Version="1.8.22" />
<PackageReference Include="Hangfire.Core" Version="1.8.22" />
<PackageReference Include="HangFire.Redis" Version="2.0.1" />
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0" />
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Masuit.Tools.Core" Version="2025.5.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />
<PackageReference Include="MiniExcel" Version="1.42.0" />
<PackageReference Include="nacos-sdk-csharp.AspNetCore" Version="1.3.10" />
<PackageReference Include="nacos-sdk-csharp.Extensions.Configuration" Version="1.3.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="QRCoder" Version="1.4.1" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.1" />
<PackageReference Include="Serilog.Sinks.PeriodicBatching" Version="5.0.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.193" />
<PackageReference Include="UserCenter.Model" Version="1.5.0" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Entities;
using YuanXuan.IM.Common.Request;
namespace YuanXuan.IM.Core.Interfaces
{
public interface IBaseService<TEntity> where TEntity : BaseEntity, new()
{
Task<bool> CreateAsync(TEntity entity);
Task<bool> CreateAsync(List<TEntity> entity);
Task<long> InsertReturnBigIdentityAsync(TEntity entity);
Task<long> CreateReturnSnowflakeIdAsync(TEntity entity);
Task<List<long>> CreateReturnSnowflakeIdAsync(List<TEntity> entity);
Task<bool> DeleteByIdsAsync(params dynamic[] ids);
Task<bool> DeleteByIdsAsync(params long[] ids);
Task<TEntity> GetByIdAsync(long id);
Task<TEntity> GetFirstAsync(System.Linq.Expressions.Expression<Func<TEntity, bool>> whereExpression);
Task<List<TEntity>> GetListAsync(System.Linq.Expressions.Expression<Func<TEntity, bool>> whereExpression = null);
Task<PageResponse<TEntity>> GetPageListAsync(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression = null);
Task<PageResponse<TEntity>> GetPageListAsync<T>(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc);
Task<PageResponse<T>> GetPageListAsync<T>(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression = null);
Task<TEntity> GetSingleAsync(System.Linq.Expressions.Expression<Func<TEntity, bool>> whereExpression);
Task<bool> UpdateAsync(TEntity entity);
Task<bool> UpdateAsync(List<TEntity> entity);
Task<bool> UpdateColumsAsync(Expression<Func<TEntity, TEntity>> columns, Expression<Func<TEntity, bool>> whereExpression);
Task<bool> AnyAsync(Expression<Func<TEntity, bool>> whereExpression);
}
}

View File

@ -0,0 +1,254 @@
using Mapster;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using YuanXuan.IM.Common.Entities;
using YuanXuan.IM.Common.Request;
using YuanXuan.IM.Core.Interfaces;
using YuanXuan.IM.Infrastructure.DBContext;
namespace YuanXuan.IM.Core.Services
{
/// <summary>
/// 服务基类
/// </summary>
//[Inject(Lifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped)]
public class BaseService<TEntity> : IBaseService<TEntity> where TEntity : BaseEntity, new()
{
//protected readonly SugarRepository<TEntity> _db;
private readonly SugarRepository<TEntity> _db;
public BaseService(SugarRepository<TEntity> repository)
{
_db = repository;
}
/// <summary>
/// 创建(自增ID)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<bool> CreateAsync(TEntity entity)
{
return await _db.InsertAsync(entity);
}
/// <summary>
/// 创建
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<bool> CreateAsync(List<TEntity> entity)
{
return await _db.InsertRangeAsync(entity);
}
/// <summary>
/// 创建(自增ID)
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<long> InsertReturnBigIdentityAsync(TEntity entity)
{
return await _db.InsertReturnBigIdentityAsync(entity);
}
/// <summary>
/// 创建并返回雪花ID
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<long> CreateReturnSnowflakeIdAsync(TEntity entity)
{
return await _db.InsertReturnSnowflakeIdAsync(entity);
}
/// <summary>
/// 创建并返回雪花ID
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<List<long>> CreateReturnSnowflakeIdAsync(List<TEntity> entity)
{
return await _db.InsertReturnSnowflakeIdAsync(entity);
}
/// <summary>
/// 物理删除
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public virtual async Task<bool> DeleteByIdsAsync(params dynamic[] ids)
{
return await _db.DeleteByIdsAsync(ids);
}
/// <summary>
/// 物理删除
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
public virtual async Task<bool> DeleteByIdsAsync(params long[] ids)
{
return await _db.Context.Deleteable<TEntity>().Where(x => ids.Contains(x.Id)).ExecuteCommandHasChangeAsync();
}
/// <summary>
/// 更新
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<bool> UpdateAsync(TEntity entity)
{
return await _db.UpdateAsync(entity);
}
/// <summary>
/// 批量更新
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual async Task<bool> UpdateAsync(List<TEntity> entity)
{
return await _db.UpdateRangeAsync(entity);
}
/// <summary>
/// 更新指定字段
/// </summary>
/// <param name="columns"></param>
/// <param name="whereExpression"></param>
/// <returns></returns>
public virtual async Task<bool> UpdateColumsAsync(Expression<Func<TEntity, TEntity>> columns, Expression<Func<TEntity, bool>> whereExpression)
{
return await _db.UpdateAsync(columns, whereExpression);
}
/// <summary>
/// 根据ID获取
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual async Task<TEntity> GetByIdAsync(long id)
{
return await _db.GetByIdAsync(id);
}
/// <summary>
/// 根据ID获取
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> whereExpression = null)
{
if (whereExpression == null)
{
return await _db.GetListAsync();
}
return await _db.GetListAsync(whereExpression);
}
/// <summary>
/// 根据条件获取单个
/// </summary>
/// <param name="whereExpression"></param>
/// <returns></returns>
public virtual async Task<TEntity> GetSingleAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await _db.GetSingleAsync(whereExpression);
}
/// <summary>
/// 根据条件获取第一个
/// </summary>
/// <param name="whereExpression"></param>
/// <returns></returns>
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await _db.GetFirstAsync(whereExpression);
}
public virtual async Task<PageResponse<TEntity>> GetPageListAsync<T>(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression, Expression<Func<TEntity, object>> orderByExpression = null, OrderByType orderByType = OrderByType.Asc)
{
var pageModel = pagination.Adapt<PageModel>();
List<TEntity> result = null;
if (whereExpression != null)
result = await _db.GetPageListAsync(whereExpression, pageModel, orderByExpression, orderByType);
else
result = await _db.GetPageListAsync(x => 1 == 1, pageModel, orderByExpression, orderByType);
return new PageResponse<TEntity>
{
Items = result,
Total = pageModel.TotalCount,
};
}
/// <summary>
/// 分页查询
/// </summary>
/// <param name="whereExpression"></param>
/// <param name="pagination"></param>
/// <returns></returns>
public virtual async Task<PageResponse<TEntity>> GetPageListAsync(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression = null)
{
var pageModel = pagination.Adapt<PageModel>();
List<TEntity> result = null;
if (whereExpression != null)
result = await _db.GetPageListAsync(whereExpression, pageModel, orderByExpression: x => x.Id, orderByType: OrderByType.Desc);
else
result = await _db.GetPageListAsync(x => 1 == 1, pageModel, orderByExpression: x => x.Id, orderByType: OrderByType.Desc);
return new PageResponse<TEntity>
{
Items = result,
Total = pageModel.TotalCount,
};
}
/// <summary>
/// 分页查询
/// </summary>
/// <param name="whereExpression"></param>
/// <param name="pagination"></param>
/// <returns></returns>
public virtual async Task<PageResponse<T>> GetPageListAsync<T>(PageRequest pagination, Expression<Func<TEntity, bool>> whereExpression)
{
var pageModel = pagination.Adapt<PageModel>();
List<TEntity> result = null;
if (whereExpression != null)
result = await _db.GetPageListAsync(whereExpression, pageModel, orderByExpression: x => x.Id, orderByType: OrderByType.Desc);
else
result = await _db.GetPageListAsync(x => 1 == 1, pageModel, orderByExpression: x => x.Id, orderByType: OrderByType.Desc);
return new PageResponse<T>
{
Items = result.Adapt<List<T>>(),
Total = pageModel.TotalCount,
};
}
public virtual async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> whereExpression)
{
return await _db.IsAnyAsync(whereExpression);
}
//protected async Task<SqlSugarTransaction> BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
//{
// return await _db.Context.AsTenant().UseTranAsync(isolationLevel);
//}
//// 针对不同业务场景使用不同隔离级别
//protected async SqlSugarTransaction BeginSerializableTransaction()
//{
// return BeginTransaction(IsolationLevel.Serializable);
//}
//protected async SqlSugarTransaction BeginRepeatableReadTransaction()
//{
// return BeginTransaction(IsolationLevel.RepeatableRead);
//}
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\YuanXuan.IM.Common\YuanXuan.IM.Common.csproj" />
<ProjectReference Include="..\YuanXuan.IM.Infrastructure\YuanXuan.IM.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Interfaces\Login\" />
<Folder Include="Services\Login\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using SqlSugar;
using YuanXuan.IM.Common.Entities;
namespace YuanXuan.IM.Infrastructure.DBContext
{
/// <summary>
/// SqlSugar通用仓储类
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class SugarRepository<TEntity> : SimpleClient<TEntity> where TEntity : BaseEntity, new()
{
public SugarRepository(ISqlSugarClient sugarClient) : base(sugarClient)
{
var db = sugarClient.AsTenant().GetConnectionWithAttr<TEntity>();
base.Context = db;
}
public IEnumerable<object> Queryable<T1, T2>(Func<object, object, JoinQueryInfos> value)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,172 @@
using FreeRedis;
using Masuit.Tools;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YuanXuan.IM.Infrastructure.Redis
{
public abstract class RedisHelper
{
private static RedisClient _instance;
/// <summary>
/// redis实例
/// </summary>
public static RedisClient Instance
{
get
{
if (_instance == null)
{
throw new Exception("使用前初始化redis静态访问类 RedisHelper.Initialization(new FreeRedis.RedisClient(\"127.0.0.1:6379,password=123,defaultDatabase=13,maxpoolsize=50,prefix=key前辍\"));");
}
return _instance;
}
}
public static void SetLoginToken(long userId, string token)
{
var key = "Login_Token";
Instance.HSet(key, userId.ToString(), token);
}
public static void DeleteLoginToken(long userId)
{
var key = "Login_Token";
Instance.HDel(key, userId.ToString());
}
/// <summary>
/// 初始化redis静态访问类 RedisHelper.Initialization(new FreeRedis.RedisClient(\"127.0.0.1:6379,password=123,defaultDatabase=13,maxpoolsize=50,prefix=key前辍\"))
/// </summary>
/// <param name="redisClient"></param>
public static void Initialization(string connectionString)
{
_instance = new FreeRedis.RedisClient(connectionString)
{
Serialize = (x) => x.ToJsonString(),
Deserialize = (x, t) => JsonConvert.DeserializeObject(x, t),
};
}
internal static ThreadLocal<Random> rnd = new ThreadLocal<Random>(() => new Random());
/// <summary>
/// 随机秒防止所有key同一时间过期雪崩
/// </summary>
/// <param name="minTimeoutSeconds">最小秒数</param>
/// <param name="maxTimeoutSeconds">最大秒数</param>
/// <returns></returns>
public static int RandomExpired(int minTimeoutSeconds, int maxTimeoutSeconds) => rnd.Value.Next(minTimeoutSeconds, maxTimeoutSeconds);
/// <summary>
/// 获取Redis分布式锁
/// </summary>
/// <param name="lockKey">锁键</param>
/// <param name="lockValue">锁值建议使用GUID</param>
/// <param name="expireSeconds">过期时间(秒)</param>
/// <returns>是否获取成功</returns>
public static bool TryLock(string lockKey, string lockValue, int expireSeconds = 30)
{
return Instance.SetNx(lockKey, lockValue, expireSeconds);
}
/// <summary>
/// 释放Redis分布式锁
/// </summary>
/// <param name="lockKey">锁键</param>
/// <param name="lockValue">锁值(必须与加锁时一致)</param>
/// <returns>是否释放成功</returns>
public static bool ReleaseLock(string lockKey, string lockValue)
{
// 使用Lua脚本确保原子性只有当锁的值匹配时才删除
const string luaScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
var result = Instance.Eval(luaScript, new[] { lockKey }, new[] { lockValue });
return (long)result == 1;
}
/// <summary>
/// 延长Redis锁的过期时间
/// </summary>
/// <param name="lockKey">锁键</param>
/// <param name="lockValue">锁值</param>
/// <param name="expireSeconds">新的过期时间(秒)</param>
/// <returns>是否续期成功</returns>
public static bool RenewLock(string lockKey, string lockValue, int expireSeconds)
{
// 使用Lua脚本确保原子性只有当锁的值匹配时才更新过期时间
const string luaScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('EXPIRE', KEYS[1], ARGV[2])
else
return 0
end";
var result = Instance.Eval(luaScript, new[] { lockKey }, new[] { lockValue, expireSeconds.ToString() });
return (long)result == 1;
}
/// <summary>
/// 异步设置键值对
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiry">过期时间</param>
/// <returns></returns>
public static async Task SetAsync(string key, object value, TimeSpan expiry)
{
await Instance.SetAsync(key, value, expiry);
}
/// <summary>
/// 异步获取字符串值
/// </summary>
/// <param name="key">键</param>
/// <returns></returns>
public static async Task<string> GetStringAsync(string key)
{
return await Instance.GetAsync<string>(key);
}
/// <summary>
/// 异步删除键
/// </summary>
/// <param name="key">键</param>
/// <returns></returns>
public static async Task DeleteAsync(string key)
{
await Instance.DelAsync(key);
}
/// <summary>
/// 异步获取值
/// </summary>
/// <typeparam name="T">类型</typeparam>
/// <param name="key">键</param>
/// <returns></returns>
public static async Task<T> GetAsync<T>(string key)
{
return await Instance.GetAsync<T>(key);
}
/// <summary>
/// 异步设置键值对(默认过期时间)
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <returns></returns>
public static async Task SetAsync(string key, object value)
{
await Instance.SetAsync(key, value);
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="FreeRedis" Version="1.5.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YuanXuan.IM.Common\YuanXuan.IM.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
namespace YuanXuan.IM.Services
{
public class Class1
{
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>