From 77778f67c160fdbbafebc3fd39fb22d04a36bf29 Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Tue, 9 Jun 2026 14:36:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9ESDK=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E3=80=81OSS=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96Redis=E4=B8=8E=E4=BB=A3=E7=A0=81=E6=89=A9?= =?UTF-8?q?=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增API版本枚举SDK项,创建SDK基础控制器 2. 添加IList、string扩展方法,新增时间戳扩展工具 3. 新增OSS配置、STS凭证服务、文件处理相关代码 4. 重构Redis缓存组件,替换StackExchange.Redis为CSRedisCore 5. 修复原有Redis操作方法调用,新增基础控制器快捷返回方法 6. 新增阿里云OSS、短信、VOD相关SDK依赖 7. 配置阿里云OSS相关参数到appsettings --- .../Extensions/DateTimeExtension.cs | 14 + .../Extensions/ObjectExtension.cs | 5 + .../Extensions/StringExtension.cs | 17 + .../Autofac/AutofacExtension.cs | 12 - .../Cache/RedisHelper.cs | 119 ------- .../Middleware/JwtAutoRefreshMiddleware.cs | 3 +- .../OSS/OSSOptions.cs | 62 ++++ .../OSS/OssCredentials.cs | 19 ++ .../OSS/OssIMMService.cs | 96 ++++++ .../OSS/OssService.cs | 307 ++++++++++++++++++ ....InteractiveMagazine.Infrastructure.csproj | 6 + .../Redis/RedisExtensions.cs | 106 ++++++ .../Enum/ApiVersionEnum.cs | 5 + .../AdminAuthService.cs | 7 +- .../WeChatAuthService.cs | 11 +- .../Controllers/BaseController.cs | 15 + .../Controllers/SDK/OssController.cs | 52 +++ .../Controllers/SDK/SDKBaseController.cs | 13 + QYZH.InteractiveMagazine.WebApi/Program.cs | 8 +- .../appsettings.json | 17 +- 20 files changed, 748 insertions(+), 146 deletions(-) delete mode 100644 QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/OSS/OssCredentials.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs create mode 100644 QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/SDK/OssController.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/SDK/SDKBaseController.cs diff --git a/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs index 4973374..a378922 100644 --- a/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs +++ b/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs @@ -7,6 +7,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions /// public static class DateTimeExtension { + /// + /// 时间戳起始日期 + /// + public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0); /// /// 转换为Unix时间戳(秒) /// @@ -84,5 +88,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions { 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); + } } } diff --git a/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs index d6b81a4..303b882 100644 --- a/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs +++ b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs @@ -90,5 +90,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions return dictionary; } + + public static bool IsNull(this IList? s) + { + return s == null || s?.Count() < 1; + } } } diff --git a/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs index 116cc88..985dbc0 100644 --- a/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs +++ b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs @@ -146,5 +146,22 @@ namespace QYZH.InteractiveMagazine.Common.Extensions return Regex.IsMatch(input, pattern); } + + /// + /// 判断字符串是否为Null、空 + /// + /// + /// + 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"); + } } } diff --git a/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs b/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs index e0a86be..74c17ce 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Autofac/AutofacExtension.cs @@ -2,7 +2,6 @@ using Autofac.Extensions.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; -using QYZH.InteractiveMagazine.Infrastructure.Cache; using QYZH.InteractiveMagazine.Infrastructure.MessageQueue; using QYZH.InteractiveMagazine.Models.Settings; using RabbitMQ.Client; @@ -27,21 +26,10 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs var assemblyNames = string.Join(".", source.Take(source.Length - 1)); containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames)); - InitializeRedis(c.Configuration); InitializeRabbitMQ(c.Configuration, containerBuilder); }); } - private static void InitializeRedis(IConfiguration configuration) - { - var redisSettings = configuration.GetSection("RedisSettings").Get(); - 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) { diff --git a/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs deleted file mode 100644 index 9fc9409..0000000 --- a/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs +++ /dev/null @@ -1,119 +0,0 @@ -using StackExchange.Redis; - -namespace QYZH.InteractiveMagazine.Infrastructure.Cache; - -/// -/// Redis操作封装 -/// -public static class RedisHelper -{ - private static string? _keyPrefix; - - /// - /// Redis连接实例 - /// - public static IConnectionMultiplexer? Connection { get; set; } - - /// - /// 设置键前缀 - /// - /// 前缀字符串 - public static void SetKeyPrefix(string prefix) - { - _keyPrefix = prefix; - } - - /// - /// 获取Redis数据库实例 - /// - /// 数据库索引 - /// IDatabase实例 - public static IDatabase GetDatabase(int db = -1) - { - if (Connection == null || !Connection.IsConnected) - { - throw new InvalidOperationException("Redis连接未初始化或已断开"); - } - - return Connection.GetDatabase(db); - } - - /// - /// 设置字符串值 - /// - /// 键 - /// 值 - /// 过期时间 - /// 是否成功 - public static async Task 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); - } - - /// - /// 获取字符串值 - /// - /// 键 - /// - public static async Task StringGetAsync(string key) - { - var db = GetDatabase(); - var prefixedKey = GetPrefixedKey(key); - return await db.StringGetAsync(prefixedKey); - } - - /// - /// 删除键 - /// - /// 键 - /// 是否成功 - public static async Task KeyDeleteAsync(string key) - { - var db = GetDatabase(); - var prefixedKey = GetPrefixedKey(key); - return await db.KeyDeleteAsync(prefixedKey); - } - - /// - /// 检查键是否存在 - /// - /// 键 - /// 是否存在 - public static async Task KeyExistsAsync(string key) - { - var db = GetDatabase(); - var prefixedKey = GetPrefixedKey(key); - return await db.KeyExistsAsync(prefixedKey); - } - - /// - /// 设置键的过期时间 - /// - /// 键 - /// 过期时间 - /// 是否成功 - public static async Task KeyExpireAsync(string key, TimeSpan expiry) - { - var db = GetDatabase(); - var prefixedKey = GetPrefixedKey(key); - return await db.KeyExpireAsync(prefixedKey, expiry); - } - - /// - /// 获取带前缀的键 - /// - /// 原始键 - /// 带前缀的键 - private static string GetPrefixedKey(string key) - { - return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}"; - } -} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs index 6679d05..f3f3681 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/JwtAutoRefreshMiddleware.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; -using QYZH.InteractiveMagazine.Infrastructure.Cache; using QYZH.InteractiveMagazine.Models.Settings; using System.IdentityModel.Tokens.Jwt; @@ -63,7 +62,7 @@ public class JwtAutoRefreshMiddleware var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken( long.Parse(userId), userName, jwtSettings); - await RedisHelper.StringSetAsync( + await RedisHelper.SetAsync( $"{TokenKeyPrefix}:{userId}", newToken, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs new file mode 100644 index 0000000..f5edadd --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs @@ -0,0 +1,62 @@ + +namespace QYZH.InteractiveMagazine.Infrastructure.OSS +{ + /// + /// OSS配置 + /// + public class OSSOptions + { + public const string Section = "AliyunOSSConfigs"; + + /// + /// ID + /// + public string AccessKeyID { get; set; } + + /// + /// 密钥 + /// + public string AccessKeySecret { get; set; } + + /// + /// 地区 + /// + public string Region { get; set; } + + /// + /// 存储桶 + /// + public string BucketName { get; set; } + + /// + /// 视频点播储存桶 + /// + public string VodBucketName { get; set; } + + /// + /// 存储桶 + /// + public string RoleArn { get; set; } + + /// + /// 过期时间 + /// + public int DurationSeconds { get; set; } + + /// + /// 终结点 + /// + public string Endpoint { get; set; } + + /// + /// IMM转换项目名称 + /// + public string ProjectName { get; set; } + + /// + /// 访问域名 + /// + public string Domain { get; set; } + } + +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssCredentials.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssCredentials.cs new file mode 100644 index 0000000..66d7d55 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssCredentials.cs @@ -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; } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs new file mode 100644 index 0000000..079b501 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs @@ -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) + { + 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 + { + { "message", _error.Message } + }); + Console.WriteLine(error.Message); + return false; + } + } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs new file mode 100644 index 0000000..e62f38f --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs @@ -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 _logger; + //private readonly IOssResourceRepository _ossResourceRepository; + public OssService(IOptionsMonitor options, CSRedisClient redis, ILogger logger) + { + // _ossOption = configuration.GetSection("AliyunOSSConfigs").Get(); + _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 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 + { + new + { + Action = new List { "oss:PutObject", "oss:GetObject","oss:DeleteObject" }, + Resource = new List { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" }, + //Condition = new + //{ + // NumericLessThanEquals = new Dictionary() + // { + // {"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(stsJson); + } + } + + public async Task> 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 + { + new + { + Action = new List { "oss:PutObject", "oss:GetObject","oss:DeleteObject" }, + Resource = new List { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" }, + //Condition = new + //{ + // NumericLessThanEquals = new Dictionary() + // { + // {"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 + { + {"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>(stsJson); + } + + /// + /// 上传单个 + /// + /// + /// + 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; + //} + } + + /// + /// 删除单个 + /// + /// + /// + 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; + } + } + + /// + /// 删除多个 + /// + /// + /// + public bool DeleteObjects(List? 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 ListObjects(string prefix) + { + try + { + var keys = new List(); + 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; + } + } + + /// + /// 复制文件 + /// + /// + /// + /// + public bool CopyObject(string sourceObject, string targetObject, long? id = null) + { + try + { + //var metadata = new ObjectMetadata(); + // 设置自定义元数据。自定义元数据为键值对(Key-Value)的形式,例如Key为mk1,Value为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; + } + + } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj b/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj index 61c1cf7..d889c8e 100644 --- a/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj +++ b/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj @@ -7,8 +7,14 @@ + + + + + + diff --git a/QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs new file mode 100644 index 0000000..fec96d7 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs @@ -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); + + /// + /// AddCSRedisCacheExtension + /// + /// + /// + /// + 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(); + services.AddSingleton(redisOptions); + var csredis = new CSRedisClient(redisOptions.ConnectionString, redisOptions.Sentinels, redisOptions.ReadOnly); + RedisHelper.Initialization(csredis); + services.AddSingleton(csredis);//csredis注册为单例 + //services.AddSingleton(new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance)); + + return services; + } + + + /// + /// 获取缓存,如果换成不存在则执行方法生成缓存 + /// + /// + /// + /// + /// + /// + public static T GetOrSet(this CSRedisClient redisCache, string key, Func factory, TimeSpan? expireTime = null) where T : class + { + var result = redisCache.Get(key); + if (factory != null && result == null) + { + result = factory(); + if (expireTime == null || !expireTime.HasValue) + expireTime = expireSecond; + + redisCache.Set(key, result, expireTime.Value); + } + return result; + } + + /// + /// 获取缓存,如果缓存不存在则执行方法生成缓存 + /// + /// + /// + /// + /// + /// + public static async Task GetOrSetAsync(this CSRedisClient redisCache, string key, Func factory, TimeSpan? expireTime = null) + { + var result = await redisCache.GetAsync(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 + { + /// + /// 连接字符串 + /// + public string ConnectionString { get; set; } + + /// + /// 哨兵设置 + /// + public string[] Sentinels { get; set; } + + /// + /// 是否只读 + /// + public bool ReadOnly { get; set; } = false; + + public int[] ExpireSecondRange { get; set; } + + public int? ExpireSecond { get; set; } = 3600; + } +} diff --git a/QYZH.InteractiveMagazine.Models/Enum/ApiVersionEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/ApiVersionEnum.cs index 9363da6..8af7eb3 100644 --- a/QYZH.InteractiveMagazine.Models/Enum/ApiVersionEnum.cs +++ b/QYZH.InteractiveMagazine.Models/Enum/ApiVersionEnum.cs @@ -21,6 +21,11 @@ namespace QYZH.InteractiveMagazine.Models.Enum /// [Description("小程序管理")] Wechat = 20, + /// + /// Wechat + /// + [Description("SDK")] + SDK = 30, } } diff --git a/QYZH.InteractiveMagazine.Service/AdminAuthService.cs b/QYZH.InteractiveMagazine.Service/AdminAuthService.cs index 0087406..ae575c0 100644 --- a/QYZH.InteractiveMagazine.Service/AdminAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/AdminAuthService.cs @@ -2,7 +2,6 @@ using BCrypt.Net; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.Infrastructure.Auth; -using QYZH.InteractiveMagazine.Infrastructure.Cache; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; @@ -57,7 +56,7 @@ public class AdminAuthService(BaseRepository adminUserRepository, ICo 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); @@ -74,7 +73,7 @@ public class AdminAuthService(BaseRepository adminUserRepository, ICo { logger.LogInformation("管理员登出,ID: {UserId}", userId); - await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}"); + await RedisHelper.DelAsync($"{TokenKeyPrefix}:{userId}"); logger.LogInformation("管理员登出成功,ID: {UserId}", userId); } @@ -134,7 +133,7 @@ public class AdminAuthService(BaseRepository adminUserRepository, ICo throw new BusinessException("修改密码失败", 500); } - await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}"); + await RedisHelper.DelAsync($"{TokenKeyPrefix}:{userId}"); logger.LogInformation("管理员修改密码成功,ID: {UserId}", userId); } diff --git a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs index 4d8e197..4513a15 100644 --- a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.Infrastructure.Auth; -using QYZH.InteractiveMagazine.Infrastructure.Cache; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Entity; @@ -220,8 +219,8 @@ public class WeChatAuthService(BaseRepository usersRepository, IConfigura var token = JwtHelper.GenerateToken((long)refreshedUser.Id, refreshedUser.Name, jwtSettings); // 清除旧用户 Token 缓存,写入新用户 Token 缓存 - await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{currentUserId}"); - await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); + await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}"); + await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); return new WeChatSwitchUserOutput { @@ -259,7 +258,7 @@ public class WeChatAuthService(BaseRepository usersRepository, IConfigura var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings); // 缓存 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(); @@ -277,7 +276,7 @@ public class WeChatAuthService(BaseRepository usersRepository, IConfigura private async Task GetAccessTokenAsync(WeChatSettings settings) { // 先从 Redis 缓存获取 - var cachedToken = await RedisHelper.StringGetAsync(AccessTokenCacheKey); + var cachedToken = await RedisHelper.GetAsync(AccessTokenCacheKey); if (!string.IsNullOrWhiteSpace(cachedToken)) { return cachedToken; @@ -296,7 +295,7 @@ public class WeChatAuthService(BaseRepository usersRepository, IConfigura // 缓存 access_token,提前 5 分钟过期(微信默认 7200 秒) 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); return response.AccessToken; diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs index 87421f0..734a48c 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs @@ -60,4 +60,19 @@ public abstract class BaseController : ControllerBase { return BaseResponse.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)); + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/OssController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/OssController.cs new file mode 100644 index 0000000..0ce35ee --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/OssController.cs @@ -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; + } + + /// + /// 获取StsToken + /// + /// + [HttpGet, Route("sts")] + public async Task Credentials() + { + var data = await _ossService.GetSTSToken(); + return Ok(BaseResponse>.Success(data)); + } + + /// + /// 删除oss文件 + /// + /// + [HttpPost, Route("delete")] + public async Task DelObject(string key) + { + // 查看用户是否有权删除 + var ok = _ossService.DeleteObject(key); + return ApiResult(ok, "删除文件失败"); + } + + /// + /// 获取上传路径前缀 + /// + /// + [HttpGet, Route("getUploadPrefix")] + public async Task GetUploadPrefix() + { + return BaseResponse.Success($"temp/{YitIdHelper.NextId()}"); + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/SDKBaseController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/SDKBaseController.cs new file mode 100644 index 0000000..3e30375 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/SDK/SDKBaseController.cs @@ -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 + { + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs index f74ac79..a8a7d40 100644 --- a/QYZH.InteractiveMagazine.WebApi/Program.cs +++ b/QYZH.InteractiveMagazine.WebApi/Program.cs @@ -12,6 +12,7 @@ using QYZH.InteractiveMagazine.Infrastructure.Autofacs; using QYZH.InteractiveMagazine.Infrastructure.Context; using QYZH.InteractiveMagazine.Infrastructure.Extensions; using QYZH.InteractiveMagazine.Infrastructure.Middleware; +using QYZH.InteractiveMagazine.Infrastructure.Redis; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Settings; @@ -34,6 +35,8 @@ YitIdHelper.SetIdGenerator(new IdGeneratorOptions() { WorkerId = 1 }); // autofac注入 允许使用autofac作为DI容器 builder.UseAutofac(); + + builder.InitSqlSugarDb(new IocConfig() { ConfigId = 0, @@ -45,7 +48,8 @@ builder.InitSqlSugarDb(new IocConfig() // 消除Error unprotecting the session cookie警告 builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection")); - +//redis +builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings")); // 配置Serilog Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) @@ -158,6 +162,7 @@ builder.Services.AddCors(options => .AllowAnyHeader(); }); }); + #region 配置数据压缩选项 // 1.首先配置压缩选项的Options> @@ -182,6 +187,7 @@ builder.Services.AddResponseCompression(options => }); #endregion + var app = builder.Build(); { diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json index 250560b..4b83df5 100644 --- a/QYZH.InteractiveMagazine.WebApi/appsettings.json +++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json @@ -10,7 +10,8 @@ }, "RedisSettings": { "ConnectionString": "192.168.20.150:16379,defaultDatabase=5", - "InstanceName": "interactive_magazine" + "Sentinels": [], + "ExpireSecondRange": [ 3600, 7200 ] }, "RabbitMQSettings": { "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/" + } }