feat: 对接腾讯云cos
All checks were successful
Build and Deploy / deploy (push) Successful in 1m21s

This commit is contained in:
xiaolipro 2026-09-16 15:47:39 +08:00
parent 946c9abaca
commit 2c3a884264
9 changed files with 88 additions and 351 deletions

View File

@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<!-- All Fake packages -->
<FakePackageVersion>8.0.5.16</FakePackageVersion>
<FakePackageVersion>8.0.5.17</FakePackageVersion>
</PropertyGroup>
</Project>

View File

@ -4,5 +4,20 @@
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"ObjectStorage": {
"Provider": "TencentCos",
"SignUrlsByDefault": true,
"DefaultSignedUrlExpiresSeconds": 900,
"DefaultUploadExpiresSeconds": 600,
"TencentCos": {
"Region": "ap-guangzhou",
"SecretId": "替换为腾讯云SecretId",
"SecretKey": "替换为腾讯云SecretKey",
"Bucket": "your-bucket-1250000000",
"UseHttps": true,
"SignUrls": true,
"DurationSecond": 600
}
}
}

View File

@ -5,9 +5,9 @@ using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using Fake.DependencyInjection;
using Fake.ObjectStorage;
using InterfaceForward.Application.Contracts.ForwardCore;
using InterfaceForward.Application.Helpers;
using InterfaceForward.Application.OSS;
using InterfaceForward.Domain.Shared;
using InterfaceForward.Domain.Shared.Enum;
using Microsoft.AspNetCore.Components;
@ -23,6 +23,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
{
private static readonly ConcurrentDictionary<string, Delegate> DelegateDic = new();
[Inject] public IHttpClientFactory ClientFactory { get; set; } = null!;
[Inject] public IObjectStorage ObjectStorage { get; set; } = null!;
public virtual Task<JToken> InParamRestructure(ForwardCoreContext context)
{
@ -90,7 +91,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
if (tokens.Count == 0) tokens = response.SelectTokens($"{pdfLabelConfig.Path}").ToList();
if (tokens.Count == 0) return mappedRes;
var token = tokens.First();
UploadResultDto oss = null!;
ObjectStorageSaveResult? oss = null;
switch (Enum.Parse<LabelType>(pdfLabelConfig.Value))
{
case LabelType.Url:
@ -98,14 +99,14 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
string url = token.Value<string>()!;
using var r = await ClientFactory.CreateClient().GetAsync(url);
await using var stream = await r.Content.ReadAsStreamAsync();
oss = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + ".pdf");
oss = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + ".pdf");
break;
}
case LabelType.Base64:
byte[] bytes = Convert.FromBase64String(token.ToString());
if (bytes.Length > 0)
{
oss = OSSHelper.Upload(new MemoryStream(bytes), Guid.NewGuid().ToString("D") + ".pdf");
oss = await SaveObjectAsync(new MemoryStream(bytes), Guid.NewGuid().ToString("D") + ".pdf");
}
break;
@ -113,8 +114,11 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
throw new ArgumentOutOfRangeException();
}
mappedRes["oss_key"] = oss.Key;
mappedRes["oss_url"] = oss.TempUrl;
if (oss != null)
{
mappedRes["oss_key"] = oss.ObjectKey;
mappedRes["oss_url"] = oss.Url;
}
}
#endregion
@ -379,19 +383,19 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
var base64 = await response.Content.ReadAsStringAsync();
byte[] pdfBytes = Convert.FromBase64String(base64);
using MemoryStream stream = new MemoryStream(pdfBytes);
var ans = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + ".pdf");
return JsonConvert.SerializeObject(ans);
var ans = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + ".pdf");
return SerializeObjectStorageResult(ans);
}
// 流
if (context.TargetInterface.ResponseContentType is ResponseContentType.csv or ResponseContentType.pdf)
{
// 上传oss
// 上传对象存储
string suffix = context.TargetInterface.ResponseContentType == ResponseContentType.csv ? "csv" : "pdf";
await using var stream = await response.Content.ReadAsStreamAsync();
var ans = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + "." + suffix);
return JsonConvert.SerializeObject(ans);
var ans = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + "." + suffix);
return SerializeObjectStorageResult(ans);
}
if (response.Content.Headers.ContentType?.CharSet?.ToUpper() == "GBK")
@ -697,4 +701,27 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency
@delegate.DynamicInvoke(context);
}
}
private Task<ObjectStorageSaveResult> SaveObjectAsync(Stream content, string objectKey)
{
return ObjectStorage.SaveAsync(new ObjectStorageSaveArgs
{
ObjectKey = objectKey,
Content = content,
Overwrite = true
});
}
/// <summary>
/// 序列化为历史字段名 Key / TempUrl,供出参映射读取。
/// </summary>
private static string SerializeObjectStorageResult(ObjectStorageSaveResult result)
{
return JsonConvert.SerializeObject(new
{
Key = result.ObjectKey,
TempUrl = result.Url,
IsSuccess = true
});
}
}

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../common.props"/>
<Import Project="../../common.props" />
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@ -8,20 +8,20 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\InterfaceForward.Application.Contracts\InterfaceForward.Application.Contracts.csproj"/>
<ProjectReference Include="..\InterfaceForward.Domain.Shared\InterfaceForward.Domain.Shared.csproj"/>
<ProjectReference Include="..\InterfaceForward.Repositories\InterfaceForward.Repositories.csproj"/>
<ProjectReference Include="..\InterfaceForward.Application.Contracts\InterfaceForward.Application.Contracts.csproj" />
<ProjectReference Include="..\InterfaceForward.Domain.Shared\InterfaceForward.Domain.Shared.csproj" />
<ProjectReference Include="..\InterfaceForward.Repositories\InterfaceForward.Repositories.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiffPlex" Version="1.9.0" />
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageReference Include="DotNetCore.Compile.Environment" Version="3.2.0" />
<PackageReference Include="DotNetCore.Natasha.CSharp.Compiler" Version="8.0.0" />
<PackageReference Include="DotNetCore.Natasha.CSharp.Compiler.Domain" Version="8.0.0" />
<PackageReference Include="Fake.AspNetCore" Version="$(FakePackageVersion)" />
<PackageReference Include="Fake.EventBus.RabbitMQ" Version="$(FakePackageVersion)" />
<PackageReference Include="Fake.ObjectMapping.AutoMapper" Version="$(FakePackageVersion)" />
<PackageReference Include="Fake.ObjectStorage.TencentCos" Version="$(FakePackageVersion)" />
<PackageReference Include="FreeRedis" Version="1.5.5" />
</ItemGroup>

View File

@ -3,6 +3,7 @@ using Fake.AspNetCore;
using Fake.EventBus.RabbitMQ;
using Fake.Modularity;
using Fake.ObjectMapping.AutoMapper;
using Fake.ObjectStorage;
using Fake.RabbitMQ;
using FreeRedis;
using InterfaceForward.Application.Contracts;
@ -21,6 +22,7 @@ namespace InterfaceForward.Application;
[DependsOn(typeof(FakeAspNetCoreModule))]
[DependsOn(typeof(FakeObjectMappingAutoMapperModule))]
[DependsOn(typeof(FakeEventBusRabbitMqModule))]
[DependsOn(typeof(FakeObjectStorageTencentCosModule))]
[DependsOn(typeof(InterfaceForwardRepositoriesModule))]
[DependsOn(typeof(InterfaceForwardDomainSharedModule))]
public class InterfaceForwardApplicationModule:FakeModule

View File

@ -1,263 +0,0 @@
using System.Security.Cryptography;
using Aliyun.OSS;
namespace InterfaceForward.Application.OSS
{
/// <summary>
/// 阿里云OSS帮助类
/// </summary>
public static class OSSHelper
{
#region 初始化
private static OSSOptions _OSSOptions;
internal static void Init(OSSOptions OSSOptions)
{
_OSSOptions = OSSOptions;
}
#endregion
#region 获取client
private static OssClient GetClient()
{
string errorMessage = null;
if (string.IsNullOrEmpty(_OSSOptions?.Endpoint))
{
errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.Endpoint)}】!";
}
if (string.IsNullOrEmpty(_OSSOptions?.AccessKeyId))
{
errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.AccessKeyId)}】!";
}
if (string.IsNullOrEmpty(_OSSOptions?.AccessKeySecret))
{
errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.AccessKeySecret)}】!";
}
if (!string.IsNullOrEmpty(errorMessage))
{
throw new Exception(errorMessage);
}
var client = new OssClient(_OSSOptions.Endpoint, _OSSOptions.AccessKeyId, _OSSOptions.AccessKeySecret);
return client;
}
#endregion
#region 上传
/// <summary>
/// 上传文件
/// </summary>
/// <param name="data">文档流</param>
/// <param name="fileName">文件名称</param>
/// <param name="usCustomKey">是否使用自定义ossKey,如果为true,会以fileName为key</param>
/// <returns></returns>
public static UploadResultDto Upload(Stream data, string fileName = "", bool usCustomKey = false, bool isPublicBucket = false)
{
if (data == null || data.Length == 0)
return new UploadResultDto(false);
var result = Private_Upload(fileName, data, usCustomKey, isPublicBucket);
return result;
}
private static UploadResultDto Private_Upload(string fileName, Stream data, bool usCustomKey = false, bool isPublicBucket = false)
{
var client = GetClient();
var ossKey = "";
if (usCustomKey)
{
ossKey = fileName;
}
else
{
var md5 = ComputeDataMd5(data).ToUpper();// 计算文件MD5值
var ext = Path.GetExtension(fileName);
ossKey = $"{md5}{ext}";
}
// 文件已经存在直接返回
var existingObject = client.DoesObjectExist(_OSSOptions.BucketName, ossKey);
if (existingObject)
{
var res = new UploadResultDto(true);
res.TempUrl = ossKey.GetUrlByKey(isPublicBucket);
res.Key = ossKey;
res.FileName = fileName;
return res;
}
// 上传文件
bool putResult = false;
using (var pubObj = client.PutObject(_OSSOptions.BucketName, ossKey, data))
{
putResult = pubObj.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
if (putResult)//上传成功
{
var result = new UploadResultDto(true);
result.FileName = fileName;
result.TempUrl = ossKey.GetUrlByKey(isPublicBucket);
result.Key = ossKey;
return result;
}
else
{
// 上传失败
var result = new UploadResultDto(false);
result.FileName = fileName;
return result;
}
}
public static string ComputeDataMd5(Stream data)
{
using (var md5 = MD5.Create())
{
var hash = BitConverter.ToString(md5.ComputeHash(data)).Replace("-", "").Replace("=", "").ToLower();
data.Position = 0;
return hash;
}
}
#endregion
#region 下载
/// <summary>
/// 通过Key获取文件的URL - 同步
/// </summary>
/// <param name="key">Key</param>
/// <returns>下载Url</returns>
public static string GetUrlByKey(this string key, bool isPublicBucket = false)
{
if (string.IsNullOrEmpty(key))
{
throw new Exception($"指定的key不能为空");
}
try
{
var client = GetClient();
Uri url = client.GeneratePresignedUri(_OSSOptions.BucketName, key);
if (isPublicBucket)
{
var urlStr = url?.GetLeftPart(UriPartial.Path);
return (_OSSOptions.ReturnHttpsUrl && urlStr?.ToString()?.StartsWith("http://") == true) ? "https://" + urlStr.ToString().Substring(7) : urlStr.ToString();
}
return (_OSSOptions.ReturnHttpsUrl && url?.ToString()?.StartsWith("http://") == true) ? "https://" + url.ToString().Substring(7) : url.ToString();
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
/// <summary>
/// 根据一组key返回url
/// </summary>
/// <param name="keys"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static Dictionary<string, string> GetUrlByKeys(params string[] keys)
{
var resDic = new Dictionary<string, string>();
if (keys == null || keys.Count() <= 0)
return resDic;
var returnHttps = _OSSOptions.ReturnHttpsUrl;
var client = GetClient();
foreach (var item in keys)
{
if (resDic.ContainsKey(item))
continue;
Uri url = client.GeneratePresignedUri(_OSSOptions.BucketName, item);
if (url == null)
continue;
resDic.Add(item, returnHttps ? url.ToString().Replace("http", "https") : url.ToString());
}
return resDic;
}
/// <summary>
/// 通过Key获取文件流
/// </summary>
/// <param name="key">Key</param>
/// <returns></returns>
public static Stream GetStreamByKey(this string key)
{
if (string.IsNullOrEmpty(key))
{
throw new Exception($"指定的key不能为空");
}
try
{
var client = GetClient();
var obj = client.GetObject(_OSSOptions.BucketName, key);
if (obj != null)
{
return obj.Content;
}
else
{
throw new Exception($"文件获取异常");
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
#endregion
#region 删除
/// <summary>
/// 删除文件
/// </summary>
/// <param name="keys"></param>
/// <returns></returns>
public static DeleteObjectsResult DeleteObjectsByKeys(params string[] keys)
{
if (keys == null || keys.Length <= 0)
return default(DeleteObjectsResult);
var client = GetClient();
var request = new DeleteObjectsRequest(_OSSOptions.BucketName, keys);
return client.DeleteObjects(request);
}
#endregion
#region 查看数据信息
/// <summary>
/// 查看文档信息
/// </summary>
/// <param name="ossKey"></param>
/// <returns></returns>
public static ObjectMetadata GetObjectMetadata(string ossKey)
{
var client = GetClient().GetObjectMetadata(_OSSOptions.BucketName, ossKey);
return client;
}
#endregion
}
}

View File

@ -1,32 +0,0 @@
namespace InterfaceForward.Application.OSS;
public class OSSOptions
{
/// <summary>
/// 秘钥Id(类似于公钥)
/// </summary>
public string AccessKeyId { get; set; }
/// <summary>
/// 访问秘钥
/// </summary>
public string AccessKeySecret { get; set; }
/// <summary>
/// OSS端点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// 存储空间名称
/// 注意,如果有多个BucketName的话,可以直接使用代码的方式设置这个值
/// </summary>
public string BucketName { get; set; }
/// <summary>
/// 返回https的url,默认false返回http
/// 阿里OSS默认支持http,https,不需要额外配置
/// </summary>
public bool ReturnHttpsUrl { get; set; }
}

View File

@ -1,30 +0,0 @@
namespace InterfaceForward.Application.OSS;
public class UploadResultDto
{
/// <summary>
/// 标识本次操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 临时URL,过期时间15min
/// </summary>
public string TempUrl { get; set; }
/// <summary>
/// 文件唯一标识Key,通常保存在数据
/// 后期所有的文件操作都是通过这个Key
/// </summary>
public string Key { get; set; }
/// <summary>
/// 文件原始名称
/// </summary>
public string FileName { get; set; }
public UploadResultDto(bool isSuccess)
{
IsSuccess = isSuccess;
}
}

View File

@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using Fake.ObjectStorage;
using Fake.UnitOfWork;
using InterfaceForward.Application.Contracts.Dtos.App;
using InterfaceForward.Application.Helpers;
using InterfaceForward.Application.OSS;
using InterfaceForward.Repositories;
using InterfaceForward.Repositories.App.Entitys;
using InterfaceForward.Repositories.App.Services;
@ -24,6 +24,7 @@ public class AppsService : ApplicationService
private readonly IAppRepository _appRepository;
private readonly IAppScopeRepository _appScopeRepository;
private readonly IBasicRepository<InterfaceEntity> _interfaceRepository;
private readonly IObjectStorage _objectStorage;
/// <summary>
///
@ -31,11 +32,13 @@ public class AppsService : ApplicationService
public AppsService(
IAppRepository appRepository
, IAppScopeRepository appScopeRepository
, IBasicRepository<InterfaceEntity> interfaceRepository)
, IBasicRepository<InterfaceEntity> interfaceRepository
, IObjectStorage objectStorage)
{
_appRepository = appRepository;
_appScopeRepository = appScopeRepository;
_interfaceRepository = interfaceRepository;
_objectStorage = objectStorage;
}
#region Get
@ -53,13 +56,26 @@ public class AppsService : ApplicationService
.OrderByDescending(a => a.UpdateTime)
.ToPagedListAsync(input.PageIndex, input.PageSize);
// var logoKeys = res.Items
// .Where(x => !x.LogoKey.IsNullOrWhiteSpace())
// .Select(x => x.LogoKey).ToArray();
// var urlDic = OSSHelper.GetUrlByKeys(logoKeys);
var logoKeys = res.Items
.Where(x => !x.LogoKey.IsNullOrWhiteSpace())
.Select(x => x.LogoKey)
.Distinct()
.ToArray();
// foreach (var url in urlDic)
// res.Items.First(x => x.LogoKey == url.Key).LogoUrl = url.Value;
if (logoKeys.Length > 0)
{
var urlDic = new Dictionary<string, string>();
foreach (var logoKey in logoKeys)
{
urlDic[logoKey] = await _objectStorage.GetUrlAsync(logoKey);
}
foreach (var item in res.Items.Where(x => !x.LogoKey.IsNullOrWhiteSpace()))
{
if (urlDic.TryGetValue(item.LogoKey, out var logoUrl))
item.LogoUrl = logoUrl;
}
}
return res;
}
@ -135,7 +151,9 @@ public class AppsService : ApplicationService
.SingleAsync(x => x.Id == id);
_ = res ?? throw new BusinessException("在指定的编号下找不到数据");
// if (res.LogoKey != null) res.LogoUrl = OSSHelper.GetUrlByKey(res.LogoKey);
if (!res.LogoKey.IsNullOrWhiteSpace())
res.LogoUrl = await _objectStorage.GetUrlAsync(res.LogoKey);
res.AppScope = await _appScopeRepository.AsQueryable()
.Where(x => x.AppId == id)
.Select(x => new AppScopeOutput { Id = x.Id.SelectAll() })