feat: 新增SDK模块、OSS相关功能并优化Redis与代码扩展

1.  新增API版本枚举SDK项,创建SDK基础控制器
2.  添加IList、string扩展方法,新增时间戳扩展工具
3.  新增OSS配置、STS凭证服务、文件处理相关代码
4.  重构Redis缓存组件,替换StackExchange.Redis为CSRedisCore
5.  修复原有Redis操作方法调用,新增基础控制器快捷返回方法
6.  新增阿里云OSS、短信、VOD相关SDK依赖
7.  配置阿里云OSS相关参数到appsettings
This commit is contained in:
glz
2026-06-09 14:36:46 +08:00
parent 1d86580db7
commit 77778f67c1
20 changed files with 748 additions and 146 deletions

View File

@ -7,6 +7,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
/// </summary> /// </summary>
public static class DateTimeExtension public static class DateTimeExtension
{ {
/// <summary>
/// 时间戳起始日期
/// </summary>
public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0);
/// <summary> /// <summary>
/// 转换为Unix时间戳(秒) /// 转换为Unix时间戳(秒)
/// </summary> /// </summary>
@ -84,5 +88,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
{ {
return dateTime.Date.AddDays(1).AddTicks(-1); return dateTime.Date.AddDays(1).AddTicks(-1);
} }
public static long ToTimestamp(this DateTime dateTime, bool milliseconds = false)
{
var timestamp = dateTime.ToUniversalTime() - TimestampStart;
return (long)(milliseconds ? timestamp.TotalMilliseconds : timestamp.TotalSeconds);
}
public static long ToMillisecondsTimestamp(this DateTime dateTime)
{
return ToTimestamp(dateTime, true);
}
} }
} }

View File

@ -90,5 +90,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
return dictionary; return dictionary;
} }
public static bool IsNull<T>(this IList<T>? s)
{
return s == null || s?.Count() < 1;
}
} }
} }

View File

@ -146,5 +146,22 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
return Regex.IsMatch(input, pattern); return Regex.IsMatch(input, pattern);
} }
/// <summary>
/// 判断字符串是否为Null、空
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static bool IsNull(this string s)
{
return string.IsNullOrWhiteSpace(s);
}
public static string RemoveDomain(this string str)
{
if (str.IsNull()) return str;
// 匹配域名后的路径部分(包括第一个/
return Regex.Replace(str, @"https?://[^/]+/([^""\s<>]*)", "$1");
}
} }
} }

View File

@ -2,7 +2,6 @@
using Autofac.Extensions.DependencyInjection; using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue; using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Models.Settings; using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client; using RabbitMQ.Client;
@ -27,21 +26,10 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
var assemblyNames = string.Join(".", source.Take(source.Length - 1)); var assemblyNames = string.Join(".", source.Take(source.Length - 1));
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames)); containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
InitializeRedis(c.Configuration);
InitializeRabbitMQ(c.Configuration, containerBuilder); InitializeRabbitMQ(c.Configuration, containerBuilder);
}); });
} }
private static void InitializeRedis(IConfiguration configuration)
{
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>();
if (redisSettings != null && !string.IsNullOrWhiteSpace(redisSettings.ConnectionString))
{
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString);
RedisHelper.Connection = multiplexer;
RedisHelper.SetKeyPrefix(redisSettings.InstanceName ?? string.Empty);
}
}
private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder) private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder)
{ {

View File

@ -1,119 +0,0 @@
using StackExchange.Redis;
namespace QYZH.InteractiveMagazine.Infrastructure.Cache;
/// <summary>
/// Redis操作封装
/// </summary>
public static class RedisHelper
{
private static string? _keyPrefix;
/// <summary>
/// Redis连接实例
/// </summary>
public static IConnectionMultiplexer? Connection { get; set; }
/// <summary>
/// 设置键前缀
/// </summary>
/// <param name="prefix">前缀字符串</param>
public static void SetKeyPrefix(string prefix)
{
_keyPrefix = prefix;
}
/// <summary>
/// 获取Redis数据库实例
/// </summary>
/// <param name="db">数据库索引</param>
/// <returns>IDatabase实例</returns>
public static IDatabase GetDatabase(int db = -1)
{
if (Connection == null || !Connection.IsConnected)
{
throw new InvalidOperationException("Redis连接未初始化或已断开");
}
return Connection.GetDatabase(db);
}
/// <summary>
/// 设置字符串值
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> StringSetAsync(string key, string value, TimeSpan? expiry = null)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
if (expiry.HasValue)
{
return await db.StringSetAsync(prefixedKey, value, (Expiration)expiry.Value);
}
return await db.StringSetAsync(prefixedKey, value);
}
/// <summary>
/// 获取字符串值
/// </summary>
/// <param name="key">键</param>
/// <returns>值</returns>
public static async Task<string?> StringGetAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.StringGetAsync(prefixedKey);
}
/// <summary>
/// 删除键
/// </summary>
/// <param name="key">键</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyDeleteAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyDeleteAsync(prefixedKey);
}
/// <summary>
/// 检查键是否存在
/// </summary>
/// <param name="key">键</param>
/// <returns>是否存在</returns>
public static async Task<bool> KeyExistsAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExistsAsync(prefixedKey);
}
/// <summary>
/// 设置键的过期时间
/// </summary>
/// <param name="key">键</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyExpireAsync(string key, TimeSpan expiry)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExpireAsync(prefixedKey, expiry);
}
/// <summary>
/// 获取带前缀的键
/// </summary>
/// <param name="key">原始键</param>
/// <returns>带前缀的键</returns>
private static string GetPrefixedKey(string key)
{
return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}";
}
}

View File

@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Models.Settings; using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
@ -63,7 +62,7 @@ public class JwtAutoRefreshMiddleware
var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken( var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken(
long.Parse(userId), userName, jwtSettings); long.Parse(userId), userName, jwtSettings);
await RedisHelper.StringSetAsync( await RedisHelper.SetAsync(
$"{TokenKeyPrefix}:{userId}", $"{TokenKeyPrefix}:{userId}",
newToken, newToken,
TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));

View File

@ -0,0 +1,62 @@

namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
/// <summary>
/// OSS配置
/// </summary>
public class OSSOptions
{
public const string Section = "AliyunOSSConfigs";
/// <summary>
/// ID
/// </summary>
public string AccessKeyID { get; set; }
/// <summary>
/// 密钥
/// </summary>
public string AccessKeySecret { get; set; }
/// <summary>
/// 地区
/// </summary>
public string Region { get; set; }
/// <summary>
/// 存储桶
/// </summary>
public string BucketName { get; set; }
/// <summary>
/// 视频点播储存桶
/// </summary>
public string VodBucketName { get; set; }
/// <summary>
/// 存储桶
/// </summary>
public string RoleArn { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public int DurationSeconds { get; set; }
/// <summary>
/// 终结点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// IMM转换项目名称
/// </summary>
public string ProjectName { get; set; }
/// <summary>
/// 访问域名
/// </summary>
public string Domain { get; set; }
}
}

View File

@ -0,0 +1,19 @@
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssCredentials
{
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string SecurityToken { get; set; }
public long ExpireTime { get; set; } // 格式化为 "yyyy-MM-dd HH:mm:ss"
public string BucketName { get; set; }
public string Region { get; set; } // 格式如 "oss-cn-hangzhou"
public string Domain { get; set; }
}
}

View File

@ -0,0 +1,96 @@
using Aliyun.Credentials.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.Web;
using Tea;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssIMMService
{
private readonly AlibabaCloud.SDK.Imm20200930.Client _client;
private readonly OSSOptions _oSSOptions;
private readonly IConfiguration _configuration;
public OssIMMService(IConfiguration configuration,IOptionsMonitor<OSSOptions> ossOptions)
{
this._configuration = configuration;
_oSSOptions = ossOptions.CurrentValue;
Config config = new Config()
{
Type = "access_key",
AccessKeyId = _oSSOptions.AccessKeyID,
AccessKeySecret = _oSSOptions.AccessKeySecret,
};
Aliyun.Credentials.Client credential = new Aliyun.Credentials.Client(config);
AlibabaCloud.OpenApiClient.Models.Config config1 = new AlibabaCloud.OpenApiClient.Models.Config
{
Credential = credential,
};
// Endpoint 请参考 https://api.aliyun.com/product/imm
config1.Endpoint = $"imm.cn-{_oSSOptions.Region}.aliyuncs.com";
_client = new AlibabaCloud.SDK.Imm20200930.Client(config1);
}
public string DocConvertPng(string source, string prefix)
{
var createOfficeConversionTaskRequest = new AlibabaCloud.SDK.Imm20200930.Models.CreateOfficeConversionTaskRequest
{
ProjectName = _oSSOptions.ProjectName,
TargetType = "png",
SourceURI = $"oss://{_oSSOptions.BucketName}/{HttpUtility.UrlDecode(source)}",
TargetURIPrefix = $"oss://{_oSSOptions.BucketName}/{prefix}",
//TargetURI = "oss://returnzero/output1/{dirname}/{barename}/{index}.{autoext}",//oss://examplebucket/outputprefix/
};
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
try
{
// 复制代码运行请自行打印 API 的返回值
var data = _client.CreateOfficeConversionTaskWithOptions(createOfficeConversionTaskRequest, runtime);
if (data?.StatusCode == 200)
{
return data.Body.TaskId;
}
return null;
}
catch (Exception _error)
{
return null;
}
}
public bool GetTask(string taskId)
{
AlibabaCloud.SDK.Imm20200930.Models.GetTaskRequest getTaskRequest = new()
{
ProjectName = _oSSOptions.ProjectName,
TaskType = "OfficeConversion",
TaskId = taskId,
};
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
try
{
var data = _client.GetTaskWithOptions(getTaskRequest, runtime);
if (data?.StatusCode == 200)
{
if (data.Body.Status == "Running") return GetTask(taskId);
if (data.Body.Status == "Succeeded") return true;
}
return false;
}
catch (Exception _error)
{
TeaException error = new TeaException(new Dictionary<string, object>
{
{ "message", _error.Message }
});
Console.WriteLine(error.Message);
return false;
}
}
}
}

View File

@ -0,0 +1,307 @@
using Aliyun.Acs.Core;
using Aliyun.Acs.Core.Auth.Sts;
using Aliyun.Acs.Core.Profile;
using Aliyun.OSS;
using Aliyun.OSS.Common;
using CSRedis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QYZH.InteractiveMagazine.Common.Extensions;
using System.Text.Json;
using System.Web;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssService
{
private readonly OssClient _ossClient;
private readonly OSSOptions _ossOption;
private readonly CSRedisClient _redis;
private readonly ILogger<OssService> _logger;
//private readonly IOssResourceRepository _ossResourceRepository;
public OssService(IOptionsMonitor<OSSOptions> options, CSRedisClient redis, ILogger<OssService> logger)
{
// _ossOption = configuration.GetSection("AliyunOSSConfigs").Get<OSSOptions>();
_ossOption = options.CurrentValue;
_ossClient = new OssClient($"oss-cn-{_ossOption.Region}.aliyuncs.com", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
_ossClient.SetRegion($"cn-{_ossOption.Region}");
_redis = redis;
_logger = logger;
//_ossResourceRepository = ossResourceRepository;
}
public async Task<OssCredentials> GetSTSTokenAsync(CancellationToken cancellationToken)
{
var stsJson = await _redis.GetAsync("STS-InteractiveMagazine-OssCredentials");
if (string.IsNullOrWhiteSpace(stsJson))
{
// ststoken
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
var policy = new
{
Version = "1",
Statement = new List<dynamic>
{
new
{
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
//Condition = new
//{
// NumericLessThanEquals = new Dictionary<string, object>()
// {
// {"oss:ContentLength",104857600 }
// }
//},
Effect = "Allow",
}
}
};
var assumeRoleRequest = new AssumeRoleRequest();
assumeRoleRequest.Policy = JsonSerializer.Serialize(policy);
assumeRoleRequest.RoleArn = _ossOption.RoleArn;
assumeRoleRequest.RoleSessionName = "oss-services";//这里的名字随便写
assumeRoleRequest.DurationSeconds = 3600;//过期时间
var response = client.GetAcsResponse(assumeRoleRequest);
var expiration = DateTime.Parse(response.Credentials.Expiration).AddMinutes(5);
var result = new OssCredentials
{
AccessKeyId = response.Credentials.AccessKeyId,
AccessKeySecret = response.Credentials.AccessKeySecret,
SecurityToken = response.Credentials.SecurityToken,
ExpireTime = expiration.ToMillisecondsTimestamp(),
BucketName = _ossOption.BucketName,
Region = $"oss-cn-{_ossOption.Region}",
Domain = _ossOption.Domain
};
await _redis.SetAsync("STS-InteractiveMagazine-OssCredentials", result, expiration - DateTime.Now);
return result;
}
else
{
return JsonSerializer.Deserialize<OssCredentials>(stsJson);
}
}
public async Task<Dictionary<string, string>> GetSTSToken()
{
var stsJson = await _redis.GetAsync("QYZH.InteractiveMagazine.STS");
if (stsJson.IsNull())
{
// ststoken
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
var policy = new
{
Version = "1",
Statement = new List<dynamic>
{
new
{
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
//Condition = new
//{
// NumericLessThanEquals = new Dictionary<string, object>()
// {
// {"oss:ContentLength",104857600 }
// }
//},
Effect = "Allow",
}
}
};
var request = new AssumeRoleRequest();
request.Policy = JsonSerializer.Serialize(policy);
request.RoleArn = _ossOption.RoleArn;
request.RoleSessionName = "oss-services";//这里的名字随便写
request.DurationSeconds = 3600;//过期时间
var response = client.GetAcsResponse(request);
var expiration = DateTime.Parse(response.Credentials.Expiration);
var result = new Dictionary<string, string>
{
{"AccessKeyId", response.Credentials.AccessKeyId},
{"AccessKeySecret",response.Credentials.AccessKeySecret },
{"SecurityToken",response.Credentials.SecurityToken },
{"Expiration", DateTime.Parse(response.Credentials.Expiration).ToString("yyyy-MM-dd HH:mm:ss") },
{"BucketName",_ossOption.BucketName },
{"Region",$"oss-cn-{_ossOption.Region }"},
{"Domain",_ossOption.Domain }
};
var seconds = (expiration - DateTime.Now).TotalSeconds - 300;
await _redis.SetAsync("QYZH.InteractiveMagazine.STS", result, TimeSpan.FromSeconds(seconds));
return result;
}
return JsonSerializer.Deserialize<Dictionary<string, string>>(stsJson);
}
/// <summary>
/// 上传单个
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string PutObject(string key, Stream content)
{
//try
//{
var result = _ossClient.PutObject(_ossOption.BucketName, key, content);
if (result.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
return key;
}
else return null;
//}
//catch (Exception ex)
//{
// return null;
//}
}
/// <summary>
/// 删除单个
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool DeleteObject(string key)
{
try
{
if (key.IsNull()) return false;
key = HttpUtility.UrlDecode(key);
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => w.Path == key).ExecuteAffrows();
var result = _ossClient.DeleteObject(_ossOption.BucketName, key);
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
/// <summary>
/// 删除多个
/// </summary>
/// <param name="keys"></param>
/// <returns></returns>
public bool DeleteObjects(List<string>? keys)
{
try
{
if (keys.IsNull()) return false;
// 设置为详细模式,返回所有删除的文件列表。
var quietMode = false;
keys = keys.Select(it => it.RemoveDomain()).ToList();
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => keys.Contains(w.Path)).ExecuteAffrows();
var request = new DeleteObjectsRequest(_ossOption.BucketName, keys, quietMode);
// 删除多个文件。
var result = _ossClient.DeleteObjects(request);
//var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path));
//AppException.ThrowIf(!del, "删除资源失败");
return result.Keys.Count() == keys.Count;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
public List<string> ListObjects(string prefix)
{
try
{
var keys = new List<string>();
ObjectListing result = null;
string nextMarker = string.Empty;
do
{
var listObjectsRequest = new ListObjectsRequest(_ossOption.BucketName)
{
Marker = nextMarker,
MaxKeys = 100,
Prefix = prefix,
};
result = _ossClient.ListObjects(listObjectsRequest);
foreach (var summary in result.ObjectSummaries)
{
keys.Add(summary.Key);
}
nextMarker = result.NextMarker;
} while (result.IsTruncated);
return keys;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return null;
}
}
/// <summary>
/// 复制文件
/// </summary>
/// <param name="sourceObject"></param>
/// <param name="targetObject"></param>
/// <returns></returns>
public bool CopyObject(string sourceObject, string targetObject, long? id = null)
{
try
{
//var metadata = new ObjectMetadata();
// 设置自定义元数据。自定义元数据为键值对Key-Value的形式例如Key为mk1Value为mv1。
//metadata.AddHeader("mk1", "mv1");
//metadata.AddHeader("mk2", "mv2");
sourceObject = sourceObject.RemoveDomain();
sourceObject = Uri.UnescapeDataString(sourceObject);
//var uow = _unitOfWorkManager.Begin();
//var data = _ossResourceRepository.Insert(new OssResource
//{
// MainId = id,
// SoucePath = sourceObject,
// Path = targetObject,
//});
//AppException.ThrowIf(data.IsNull(), "资源添加失败");
var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject)
{
// 如果NewObjectMetadata为null则为COPY模式即拷贝源文件的元数据非null则为REPLACE模式覆盖源文件的元数据
NewObjectMetadata = null
};
var result = _ossClient.CopyObject(req);
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
}
}

View File

@ -7,8 +7,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
<PackageReference Include="AlibabaCloud.SDK.Imm20200930" Version="4.6.4" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.8.2" />
<PackageReference Include="aliyun-net-sdk-sts" Version="3.1.2" />
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageReference Include="Autofac" Version="9.1.0" /> <PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="CSRedisCore" Version="3.8.807" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" /> <PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />

View File

@ -0,0 +1,106 @@
using CSRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace QYZH.InteractiveMagazine.Infrastructure.Redis
{
public static class RedisExtensions
{
public static TimeSpan expireSecond = TimeSpan.FromMinutes(20);
/// <summary>
/// AddCSRedisCacheExtension
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
public static IServiceCollection AddCSRedisCacheExtension(this IServiceCollection services, IConfiguration configuration)
{
if (services == null)
throw new ArgumentNullException("services");
if (configuration == null)
throw new ArgumentNullException("configuration");
var redisOptions = configuration.Get<RedisCacheOption>();
services.AddSingleton(redisOptions);
var csredis = new CSRedisClient(redisOptions.ConnectionString, redisOptions.Sentinels, redisOptions.ReadOnly);
RedisHelper.Initialization(csredis);
services.AddSingleton(csredis);//csredis注册为单例
//services.AddSingleton<IDistributedCache>(new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance));
return services;
}
/// <summary>
/// 获取缓存,如果换成不存在则执行方法生成缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="validFor"></param>
/// <returns></returns>
public static T GetOrSet<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null) where T : class
{
var result = redisCache.Get<T>(key);
if (factory != null && result == null)
{
result = factory();
if (expireTime == null || !expireTime.HasValue)
expireTime = expireSecond;
redisCache.Set(key, result, expireTime.Value);
}
return result;
}
/// <summary>
/// 获取缓存,如果缓存不存在则执行方法生成缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="validFor"></param>
/// <returns></returns>
public static async Task<T> GetOrSetAsync<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null)
{
var result = await redisCache.GetAsync<T>(key);
if (result == null && factory != null)
{
result = factory();
if (expireTime == null || !expireTime.HasValue)
expireTime = expireSecond;
if (result != null)
await redisCache.SetAsync(key, result, expireTime.Value);
}
return result;
}
}
public class RedisCacheOption
{
/// <summary>
/// 连接字符串
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// 哨兵设置
/// </summary>
public string[] Sentinels { get; set; }
/// <summary>
/// 是否只读
/// </summary>
public bool ReadOnly { get; set; } = false;
public int[] ExpireSecondRange { get; set; }
public int? ExpireSecond { get; set; } = 3600;
}
}

View File

@ -21,6 +21,11 @@ namespace QYZH.InteractiveMagazine.Models.Enum
/// </summary> /// </summary>
[Description("小程序管理")] [Description("小程序管理")]
Wechat = 20, Wechat = 20,
/// <summary>
/// Wechat
/// </summary>
[Description("SDK")]
SDK = 30,
} }
} }

View File

@ -2,7 +2,6 @@ using BCrypt.Net;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Infrastructure.Auth; using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
@ -57,7 +56,7 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
var token = JwtHelper.GenerateToken((long)adminUser.Id, adminUser.UserName, jwtSettings); var token = JwtHelper.GenerateToken((long)adminUser.Id, adminUser.UserName, jwtSettings);
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{adminUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); await RedisHelper.SetAsync($"{TokenKeyPrefix}:{adminUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
logger.LogInformation("管理员登录成功,用户名: {UserName}, ID: {UserId}", input.UserName, adminUser.Id); logger.LogInformation("管理员登录成功,用户名: {UserName}, ID: {UserId}", input.UserName, adminUser.Id);
@ -74,7 +73,7 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
{ {
logger.LogInformation("管理员登出ID: {UserId}", userId); logger.LogInformation("管理员登出ID: {UserId}", userId);
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}"); await RedisHelper.DelAsync($"{TokenKeyPrefix}:{userId}");
logger.LogInformation("管理员登出成功ID: {UserId}", userId); logger.LogInformation("管理员登出成功ID: {UserId}", userId);
} }
@ -134,7 +133,7 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
throw new BusinessException("修改密码失败", 500); throw new BusinessException("修改密码失败", 500);
} }
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}"); await RedisHelper.DelAsync($"{TokenKeyPrefix}:{userId}");
logger.LogInformation("管理员修改密码成功ID: {UserId}", userId); logger.LogInformation("管理员修改密码成功ID: {UserId}", userId);
} }

View File

@ -2,7 +2,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.Auth; using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Entity;
@ -220,8 +219,8 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
var token = JwtHelper.GenerateToken((long)refreshedUser.Id, refreshedUser.Name, jwtSettings); var token = JwtHelper.GenerateToken((long)refreshedUser.Id, refreshedUser.Name, jwtSettings);
// 清除旧用户 Token 缓存,写入新用户 Token 缓存 // 清除旧用户 Token 缓存,写入新用户 Token 缓存
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{currentUserId}"); await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}");
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatSwitchUserOutput return new WeChatSwitchUserOutput
{ {
@ -259,7 +258,7 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings); var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
// 缓存 Token 到 Redis // 缓存 Token 到 Redis
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); await RedisHelper.SetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
var userOutputs = users.Select(MapUserToOutput).ToList(); var userOutputs = users.Select(MapUserToOutput).ToList();
@ -277,7 +276,7 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
private async Task<string> GetAccessTokenAsync(WeChatSettings settings) private async Task<string> GetAccessTokenAsync(WeChatSettings settings)
{ {
// 先从 Redis 缓存获取 // 先从 Redis 缓存获取
var cachedToken = await RedisHelper.StringGetAsync(AccessTokenCacheKey); var cachedToken = await RedisHelper.GetAsync(AccessTokenCacheKey);
if (!string.IsNullOrWhiteSpace(cachedToken)) if (!string.IsNullOrWhiteSpace(cachedToken))
{ {
return cachedToken; return cachedToken;
@ -296,7 +295,7 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
// 缓存 access_token提前 5 分钟过期(微信默认 7200 秒) // 缓存 access_token提前 5 分钟过期(微信默认 7200 秒)
var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn; var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn;
await RedisHelper.StringSetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn)); await RedisHelper.SetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn));
logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn); logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn);
return response.AccessToken; return response.AccessToken;

View File

@ -60,4 +60,19 @@ public abstract class BaseController : ControllerBase
{ {
return BaseResponse<object>.Fail(ResultCode.GLOBAL_ERROR, message); return BaseResponse<object>.Fail(ResultCode.GLOBAL_ERROR, message);
} }
protected IActionResult ApiResult(bool success, string msg)
{
return success ? Success() : Fail(msg);
}
protected IActionResult Success()
{
return Ok(BaseResponse.Success());
}
protected IActionResult Fail(string msg)
{
return Ok(BaseResponse.Fail(msg));
}
} }

View File

@ -0,0 +1,52 @@

using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.Models.Dto;
using Yitter.IdGenerator;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.SDK
{
public class OssController : SDKBaseController
{
private readonly OssService _ossService;
public OssController(OssService ossService)
{
this._ossService = ossService;
}
/// <summary>
/// 获取StsToken
/// </summary>
/// <returns></returns>
[HttpGet, Route("sts")]
public async Task<IActionResult> Credentials()
{
var data = await _ossService.GetSTSToken();
return Ok(BaseResponse<Dictionary<string, string>>.Success(data));
}
/// <summary>
/// 删除oss文件
/// </summary>
/// <returns></returns>
[HttpPost, Route("delete")]
public async Task<IActionResult> DelObject(string key)
{
// 查看用户是否有权删除
var ok = _ossService.DeleteObject(key);
return ApiResult(ok, "删除文件失败");
}
/// <summary>
/// 获取上传路径前缀
/// </summary>
/// <returns></returns>
[HttpGet, Route("getUploadPrefix")]
public async Task<BaseResponse> GetUploadPrefix()
{
return BaseResponse<string>.Success($"temp/{YitIdHelper.NextId()}");
}
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.SDK
{
[Route("api/sdk/[controller]")]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.SDK))]
[AllowAnonymous]
public class SDKBaseController : BaseController
{
}
}

View File

@ -12,6 +12,7 @@ using QYZH.InteractiveMagazine.Infrastructure.Autofacs;
using QYZH.InteractiveMagazine.Infrastructure.Context; using QYZH.InteractiveMagazine.Infrastructure.Context;
using QYZH.InteractiveMagazine.Infrastructure.Extensions; using QYZH.InteractiveMagazine.Infrastructure.Extensions;
using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Infrastructure.Redis;
using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Settings; using QYZH.InteractiveMagazine.Models.Settings;
@ -34,6 +35,8 @@ YitIdHelper.SetIdGenerator(new IdGeneratorOptions() { WorkerId = 1 });
// autofac注入 允许使用autofac作为DI容器 // autofac注入 允许使用autofac作为DI容器
builder.UseAutofac(); builder.UseAutofac();
builder.InitSqlSugarDb(new IocConfig() builder.InitSqlSugarDb(new IocConfig()
{ {
ConfigId = 0, ConfigId = 0,
@ -45,7 +48,8 @@ builder.InitSqlSugarDb(new IocConfig()
// 消除Error unprotecting the session cookie警告 // 消除Error unprotecting the session cookie警告
builder.Services.AddDataProtection() builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection")); .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection"));
//redis
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
// 配置Serilog // 配置Serilog
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration) .ReadFrom.Configuration(builder.Configuration)
@ -158,6 +162,7 @@ builder.Services.AddCors(options =>
.AllowAnyHeader(); .AllowAnyHeader();
}); });
}); });
#region #region
// 1.首先配置压缩选项的Options> // 1.首先配置压缩选项的Options>
@ -182,6 +187,7 @@ builder.Services.AddResponseCompression(options =>
}); });
#endregion #endregion
var app = builder.Build(); var app = builder.Build();
{ {

View File

@ -10,7 +10,8 @@
}, },
"RedisSettings": { "RedisSettings": {
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5", "ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
"InstanceName": "interactive_magazine" "Sentinels": [],
"ExpireSecondRange": [ 3600, 7200 ]
}, },
"RabbitMQSettings": { "RabbitMQSettings": {
"HostName": "192.168.20.150", "HostName": "192.168.20.150",
@ -44,5 +45,17 @@
} }
] ]
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"AliyunOSSConfigs": {
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
"AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf",
"VodBucketName": "outin-5277bbb52bec11f08dbd00163e169e2b.oss-cn-beijing.aliyuncs.com",
"BucketName": "qyzh-qb",
"Region": "beijing",
"RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole",
"DurationSeconds": 3600, //过期时间(秒)
"Endpoint": "oss-cn-beijing.aliyuncs.com",
"ProjectName": "QuestionBank",
"Domain": "https://qyzh-qb.oss-cn-beijing.aliyuncs.com/"
}
} }