diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 9d38b61..e2f0a9a 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -1,10 +1,10 @@
-# 复制到 admin .gitea/workflows/deploy.yml
+# 复制到 admin .gitea/workflows/deploy.yml
+# push master → prod;push test → test
name: Build and Deploy
on:
push:
- branches: [master, main]
- workflow_dispatch:
+ branches: [master, test]
env:
CSPROJ: src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
@@ -21,14 +21,47 @@ jobs:
- name: Build and deploy
run: |
set -euxo pipefail
- export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
- TAG="${{ gitea.sha }}"
- IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
+ REF="${{ gitea.ref_name }}"
+ case "$REF" in
+ test) TARGET=test ;;
+ master) TARGET=prod ;;
+ *) echo "unsupported branch: $REF"; exit 1 ;;
+ esac
+
+ SHA="${{ gitea.sha }}"
REPO="${{ gitea.repository }}"
test -n "$REPO" && [[ "$REPO" == */* ]]
- echo "REPO=$REPO TAG=$TAG host=$(hostname)"
+
+ case "$TARGET" in
+ test) TAG="test-${SHA}" ;;
+ *) TAG="${SHA}" ;;
+ esac
+
+ IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
+ echo "TARGET=$TARGET REF=$REF REPO=$REPO TAG=$TAG host=$(hostname)"
+
+ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
+ unset NAMESPACE
cd /data/k3s
./scripts/ci-publish-container.sh "$REPO" "${{ env.CSPROJ }}" \
- "${{ env.IMAGE_NAME }}" "$TAG" "$TAG"
+ "${{ env.IMAGE_NAME }}" "$TAG" "$SHA"
+
+ case "$TARGET" in
+ test)
+ export KUBECONFIG=/etc/rancher/k3s/k3s-test.yaml
+ export NAMESPACE=test
+ export MANIFEST_ROOT=/data/k3s-test
+ ;;
+ *)
+ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
+ export NAMESPACE=prod
+ export MANIFEST_ROOT=/data/k3s
+ ;;
+ esac
+ if [[ ! -f "$KUBECONFIG" ]]; then
+ echo "FAIL: missing KUBECONFIG=$KUBECONFIG host=$(hostname) uid=$(id -u)"
+ ls -la /etc/rancher/k3s/ /root/.kube/ 2>/dev/null || true
+ exit 1
+ fi
./scripts/ci-deploy-api.sh "${{ env.DEPLOY_NAME }}" \
"${{ env.CONTAINER_NAME }}" "$IMG"
diff --git a/Directory.Build.props b/Directory.Build.props
index e7da47d..7d6f04d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 8.0.5.16
+ 8.0.5.17
diff --git a/src/Oms.Application/Labeling/LabelPdfGenerationService.cs b/src/Oms.Application/Labeling/LabelPdfGenerationService.cs
index 8b3516b..a6da67b 100644
--- a/src/Oms.Application/Labeling/LabelPdfGenerationService.cs
+++ b/src/Oms.Application/Labeling/LabelPdfGenerationService.cs
@@ -1,4 +1,5 @@
using Fake.Application;
+using Fake.ObjectStorage;
using Microsoft.Extensions.Logging;
using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders;
@@ -22,7 +23,7 @@ public sealed class LabelPdfGenerationService(
ISystemOtherConfigRepository systemOtherConfigRepository,
IOrderRepository orderRepository,
OrderFileManager orderFileManager,
- IFileStorage fileStorage,
+ IObjectStorage fileStorage,
ILogger logger)
{
public async Task GenerateAndPersistAsync(
@@ -167,7 +168,7 @@ public sealed class LabelPdfGenerationService(
var fileName = $"{safeNo}-{DateTime.Now:yyyyMMddHHmmss}.pdf";
var relativeKey = $"uploads/prints/orders/{fileName}";
await using var stream = new MemoryStream(pdfBytes);
- return await fileStorage.SaveAsync(stream, relativeKey, "application/pdf", cancellationToken);
+ return await fileStorage.SaveObjectAsync(stream, relativeKey, "application/pdf", cancellationToken);
}
}
diff --git a/src/Oms.Application/Oms.Application.csproj b/src/Oms.Application/Oms.Application.csproj
index 8783a7d..73214d1 100644
--- a/src/Oms.Application/Oms.Application.csproj
+++ b/src/Oms.Application/Oms.Application.csproj
@@ -20,6 +20,7 @@
+
@@ -29,8 +30,6 @@
-
-
diff --git a/src/Oms.Application/OmsApplicationModule.cs b/src/Oms.Application/OmsApplicationModule.cs
index 7603018..acd9b8a 100644
--- a/src/Oms.Application/OmsApplicationModule.cs
+++ b/src/Oms.Application/OmsApplicationModule.cs
@@ -13,7 +13,6 @@ using Oms.Application.Integrations.PlatformShop;
using Oms.Application.Labeling;
using Oms.Application.Labeling.Hiprint;
using Oms.Application.Shared;
-using Oms.Application.Storage;
using Oms.Application.Workers;
using Oms.Domain;
@@ -21,6 +20,7 @@ namespace Oms.Application;
///
/// OMS 应用层模块注册。
+/// 对象存储由 Api 宿主注册 Fake.ObjectStorage(Local / TencentCos);文件元数据走 Fake.FileManagement。
///
[DependsOn(
typeof(OmsDomainModule),
@@ -52,7 +52,6 @@ public class OmsApplicationModule : FakeModule
context.Services.AddTransient();
context.Services.AddTransient();
- context.Services.AddOmsFileStorage(configuration);
context.Services.AddOmsSmallBagTrack(configuration);
context.Services.AddOmsItfxIntegration(configuration);
context.Services.AddOmsPlatformShopIntegration(configuration);
diff --git a/src/Oms.Application/Services/Orders/OrderFileManager.cs b/src/Oms.Application/Services/Orders/OrderFileManager.cs
index b4ae163..93c8fc5 100644
--- a/src/Oms.Application/Services/Orders/OrderFileManager.cs
+++ b/src/Oms.Application/Services/Orders/OrderFileManager.cs
@@ -1,5 +1,6 @@
using Fake.Application;
using Fake.DependencyInjection;
+using Fake.ObjectStorage;
using Microsoft.AspNetCore.Http;
using Oms.Application.Dtos.Orders;
using Oms.Application.Storage;
@@ -14,7 +15,7 @@ namespace Oms.Application.Services.Orders;
///
public sealed class OrderFileManager(
IOrderFileRepository orderFileRepository,
- IFileStorage fileStorage) : IScopedDependency
+ IObjectStorage fileStorage) : IScopedDependency
{
private static readonly HashSet ImageExt =
new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp" };
@@ -94,7 +95,7 @@ public sealed class OrderFileManager(
OrderId = orderId,
FileType = OrderFileTypeCodes.SystemLabel,
FileName = fileName,
- FileUrl = fileStorage.ToStorageKey(fileUrl) ?? fileUrl.Trim(),
+ FileUrl = OmsObjectStorageExtensions.ToStorageKey(fileUrl) ?? fileUrl.Trim(),
ContentType = contentType,
FileSize = fileSize,
Sort = 0,
@@ -143,7 +144,7 @@ public sealed class OrderFileManager(
OrderId = orderId,
FileType = OrderFileTypeCodes.DownstreamLabel,
FileName = TryFileNameFromUrl(fileUrl),
- FileUrl = fileStorage.ToStorageKey(fileUrl) ?? fileUrl.Trim(),
+ FileUrl = OmsObjectStorageExtensions.ToStorageKey(fileUrl) ?? fileUrl.Trim(),
ContentType = null,
Sort = existing.Count,
Source = OrderFileSourceCodes.Downstream,
@@ -305,7 +306,7 @@ public sealed class OrderFileManager(
var storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}";
await using var stream = file.OpenReadStream();
- var key = await fileStorage.SaveAsync(stream, relativeKey, file.ContentType);
+ var key = await fileStorage.SaveObjectAsync(stream, relativeKey, file.ContentType);
return (key, Path.GetFileName(file.FileName), file.ContentType, file.Length);
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index fc27a3d..63a6b9c 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -488,12 +488,12 @@ public partial class OrderService
throw new BusinessException("自定义模板样例文件不存在,请重新上传");
}
- if (!await FileStorage.ExistsAsync(template.SourceFilePath))
+ if (!await FileStorage.ExistsAsync(FileStoragePath.NormalizeKey(template.SourceFilePath)))
{
throw new BusinessException("自定义模板样例文件不存在,请重新上传");
}
- await using var stream = await FileStorage.OpenReadAsync(template.SourceFilePath);
+ await using var stream = await FileStorage.GetAsync(FileStoragePath.NormalizeKey(template.SourceFilePath));
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var bytes = ms.ToArray();
@@ -628,7 +628,7 @@ public partial class OrderService
""";
var relativeKey = $"uploads/prints/orders/{fileName}";
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(html));
- var key = await FileStorage.SaveAsync(stream, relativeKey, "text/html; charset=utf-8");
+ var key = await FileStorage.SaveObjectAsync(stream, relativeKey, "text/html; charset=utf-8");
return FileStorage.GetPublicUrl(key);
}
@@ -647,7 +647,7 @@ public partial class OrderService
private async Task CleanupExpiredLocalExportFilesAsync()
{
// COS 侧建议用生命周期规则清理;本地仍清理 24h 前导出文件。
- if (FileStorage is not LocalFileStorage)
+ if (!FileStorage.IsLocalProvider())
{
return;
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
index 09ec87b..04b0668 100644
--- a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
+using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index 91bff8f..a7c7043 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -1,5 +1,6 @@
using Fake.Application;
using Fake.Domain.Exceptions;
+using Fake.ObjectStorage;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
@@ -35,7 +36,7 @@ public partial class OrderService : ApplicationService
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService();
private IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService();
- private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService();
+ private IObjectStorage FileStorage => LazyServiceProvider.GetRequiredService();
private LabelPdfGenerationService LabelPdfGenerationService =>
LazyServiceProvider.GetRequiredService();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
@@ -345,7 +346,7 @@ public partial class OrderService : ApplicationService
await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, await BuildOrderExportRowsAsync(orders));
stream.Position = 0;
- var storageKey = await FileStorage.SaveAsync(
+ var storageKey = await FileStorage.SaveObjectAsync(
stream,
relativeKey,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
diff --git a/src/Oms.Application/Services/Rbac/OmsUserService.cs b/src/Oms.Application/Services/Rbac/OmsUserService.cs
index 45ae3aa..d4f4a2e 100644
--- a/src/Oms.Application/Services/Rbac/OmsUserService.cs
+++ b/src/Oms.Application/Services/Rbac/OmsUserService.cs
@@ -6,6 +6,7 @@ using Fake.Rbac.Application.Dtos.User;
using Fake.Rbac.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using Fake.ObjectStorage;
using Oms.Application.Dtos.Rbac;
using Oms.Application.Rbac;
using Oms.Application.Storage;
@@ -24,7 +25,7 @@ public class OmsUserService(
UserService userService,
UserProfileRepository profileRepository,
IEmployeeRepository employeeRepository,
- IFileStorage fileStorage) : ApplicationService
+ IObjectStorage fileStorage) : ApplicationService
{
///
/// 获取用户详情(含扩展资料)。
@@ -98,7 +99,7 @@ public class OmsUserService(
Account = input.Account,
Password = input.Password,
Email = input.Email,
- Avatar = fileStorage.ToStorageKey(input.Avatar),
+ Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
OrganizationId = input.OrganizationId,
RoleIds = input.RoleIds,
}, cancellationToken);
@@ -136,7 +137,7 @@ public class OmsUserService(
{
Name = input.Name,
Email = input.Email,
- Avatar = fileStorage.ToStorageKey(input.Avatar),
+ Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
OrganizationId = input.OrganizationId,
},
cancellationToken);
@@ -174,7 +175,7 @@ public class OmsUserService(
[FromQuery] Guid id,
[FromQuery] string avatarUrl,
CancellationToken cancellationToken = default) =>
- userService.UpdateAvatarAsync(id, fileStorage.ToStorageKey(avatarUrl) ?? avatarUrl, cancellationToken);
+ userService.UpdateAvatarAsync(id, OmsObjectStorageExtensions.ToStorageKey(avatarUrl) ?? avatarUrl, cancellationToken);
///
/// 删除用户。
diff --git a/src/Oms.Application/Services/Recharge/RechargeService.cs b/src/Oms.Application/Services/Recharge/RechargeService.cs
index 0479431..200f43a 100644
--- a/src/Oms.Application/Services/Recharge/RechargeService.cs
+++ b/src/Oms.Application/Services/Recharge/RechargeService.cs
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
using Oms.Application.Dtos.Recharge;
using Oms.Application.Dtos.Shared;
+using Fake.ObjectStorage;
using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.CustomersAggregate.Repositories;
@@ -22,7 +23,7 @@ public class RechargeService : ApplicationService
{
private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService();
- private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService();
+ private IObjectStorage FileStorage => LazyServiceProvider.GetRequiredService();
///
/// 提交充值申请。
diff --git a/src/Oms.Application/Services/Storage/FileStorageService.cs b/src/Oms.Application/Services/Storage/FileStorageService.cs
deleted file mode 100644
index f2d91b0..0000000
--- a/src/Oms.Application/Services/Storage/FileStorageService.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-using Fake.Application;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using Oms.Application.Storage;
-
-namespace Oms.Application.Services.Storage;
-
-///
-/// 文件存储直传 / 临时访问。私有桶:前端拿凭证直传 COS,后端只存 key。
-///
-[Authorize]
-[ApiExplorerSettings(GroupName = "OMS-Storage")]
-public class FileStorageService : ApplicationService
-{
- private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService();
-
- ///
- /// 是否支持前端直传(Cos=true)。
- ///
- [HttpGet("/file-storage/capabilities")]
- public object GetCapabilities() => new
- {
- supportsClientDirectUpload = FileStorage.SupportsClientDirectUpload,
- provider = FileStorage.GetType().Name
- };
-
- ///
- /// 签发预签名 PUT 上传地址。前端对该 URL 直接 PUT 文件体,成功后把返回的 key 交给业务接口落库。
- ///
- [HttpPost("/file-storage/presign-upload")]
- public async Task PresignUploadAsync([FromBody] PresignUploadInput input)
- {
- if (string.IsNullOrWhiteSpace(input.FileName))
- {
- throw new BusinessException("fileName 不能为空");
- }
-
- var ticket = await FileStorage.CreatePresignedUploadAsync(
- input.Folder ?? "misc",
- input.FileName.Trim(),
- input.ContentType);
-
- return new PresignUploadOutput
- {
- Key = ticket.Key,
- UploadUrl = ticket.UploadUrl,
- Method = ticket.Method,
- ContentType = ticket.ContentType,
- Headers = ticket.Headers,
- ExpireAt = ticket.ExpireAt
- };
- }
-
- ///
- /// 签发 STS 临时密钥。前端用 cos-js-sdk-v5 等 SDK 直传;落库只存 key。
- ///
- [HttpPost("/file-storage/sts-upload")]
- public async Task StsUploadAsync([FromBody] PresignUploadInput input)
- {
- if (string.IsNullOrWhiteSpace(input.FileName))
- {
- throw new BusinessException("fileName 不能为空");
- }
-
- var ticket = await FileStorage.CreateStsUploadAsync(
- input.Folder ?? "misc",
- input.FileName.Trim());
-
- return new StsUploadOutput
- {
- Key = ticket.Key,
- CosKey = ticket.CosKey,
- Bucket = ticket.Bucket,
- Region = ticket.Region,
- TmpSecretId = ticket.TmpSecretId,
- TmpSecretKey = ticket.TmpSecretKey,
- SessionToken = ticket.SessionToken,
- StartTime = ticket.StartTime,
- ExpiredTime = ticket.ExpiredTime
- };
- }
-
- ///
- /// 按库内 key 生成临时访问地址(私有桶预签名 GET)。
- ///
- [HttpGet("/file-storage/access-url")]
- public AccessUrlOutput GetAccessUrl([FromQuery] string key)
- {
- if (string.IsNullOrWhiteSpace(key))
- {
- throw new BusinessException("key 不能为空");
- }
-
- var storageKey = FileStorage.ToStorageKey(key) ?? key.Trim();
- return new AccessUrlOutput
- {
- Key = storageKey,
- Url = FileStorage.GetPublicUrl(storageKey)
- };
- }
-
- ///
- /// 将完整 URL / 相对路径规范为库内 key(业务保存前可调用)。
- ///
- [HttpPost("/file-storage/normalize-key")]
- public NormalizeKeyOutput NormalizeKey([FromBody] NormalizeKeyInput input)
- {
- var key = FileStorage.ToStorageKey(input.PathOrUrl);
- return new NormalizeKeyOutput { Key = key };
- }
-}
-
-public class PresignUploadInput
-{
- /// 业务目录:avatars / sku / parcel / recharge / order-files / xhs 等。
- public string? Folder { get; set; }
-
- public string FileName { get; set; } = string.Empty;
-
- public string? ContentType { get; set; }
-}
-
-public class PresignUploadOutput
-{
- public string Key { get; set; } = string.Empty;
- public string UploadUrl { get; set; } = string.Empty;
- public string Method { get; set; } = "PUT";
- public string? ContentType { get; set; }
- public IReadOnlyDictionary Headers { get; set; } =
- new Dictionary();
- public DateTime ExpireAt { get; set; }
-}
-
-public class StsUploadOutput
-{
- public string Key { get; set; } = string.Empty;
- public string CosKey { get; set; } = string.Empty;
- public string Bucket { get; set; } = string.Empty;
- public string Region { get; set; } = string.Empty;
- public string TmpSecretId { get; set; } = string.Empty;
- public string TmpSecretKey { get; set; } = string.Empty;
- public string SessionToken { get; set; } = string.Empty;
- public long StartTime { get; set; }
- public long ExpiredTime { get; set; }
-}
-
-public class AccessUrlOutput
-{
- public string Key { get; set; } = string.Empty;
- public string Url { get; set; } = string.Empty;
-}
-
-public class NormalizeKeyInput
-{
- public string? PathOrUrl { get; set; }
-}
-
-public class NormalizeKeyOutput
-{
- public string? Key { get; set; }
-}
diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs
index 156cc0e..af27b0f 100644
--- a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs
+++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs
@@ -178,7 +178,7 @@ public partial class WarehouseService
throw new BusinessException("商品数量必须大于 0");
}
- var imageUrl = FileStorage.ToStorageKey(itemInput.ImageUrl);
+ var imageUrl = OmsObjectStorageExtensions.ToStorageKey(itemInput.ImageUrl);
if (imageUrl != null && imageUrl.Length > 1024)
{
imageUrl = imageUrl[..1024];
diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
index 4f16e2e..6a87085 100644
--- a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
+++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
+using Fake.ObjectStorage;
using Oms.Application.Shared;
using Oms.Application.Storage;
@@ -11,8 +12,8 @@ namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
- private IFileStorage FileStorage =>
- LazyServiceProvider.GetRequiredService();
+ private IObjectStorage FileStorage =>
+ LazyServiceProvider.GetRequiredService();
///
/// 查询 SKU 列表。
@@ -117,7 +118,7 @@ public partial class WarehouseService
entity.SkuCode = skuCode;
entity.SkuName = skuName;
entity.SkuNameEn = string.IsNullOrWhiteSpace(input.SkuNameEn) ? null : input.SkuNameEn.Trim();
- entity.SkuImageUrl = FileStorage.ToStorageKey(input.SkuImageUrl);
+ entity.SkuImageUrl = OmsObjectStorageExtensions.ToStorageKey(input.SkuImageUrl);
entity.UnitPrice = input.UnitPrice;
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
diff --git a/src/Oms.Application/Services/Xhs/XhsMediaService.cs b/src/Oms.Application/Services/Xhs/XhsMediaService.cs
index c4feb9c..d22ff8f 100644
--- a/src/Oms.Application/Services/Xhs/XhsMediaService.cs
+++ b/src/Oms.Application/Services/Xhs/XhsMediaService.cs
@@ -1,4 +1,5 @@
using Fake.Application;
+using Fake.ObjectStorage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Xhs;
@@ -15,7 +16,7 @@ public class XhsMediaService : ApplicationService
{
private IXhsMediaRepository MediaRepository => LazyServiceProvider.GetRequiredService();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService();
- private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService();
+ private IObjectStorage FileStorage => LazyServiceProvider.GetRequiredService();
///
/// 上传媒体文件
@@ -34,7 +35,7 @@ public class XhsMediaService : ApplicationService
var relativeKey = $"uploads/xhs/{customerId}/{storedName}";
await using var stream = file.OpenReadStream();
- var storageKey = await FileStorage.SaveAsync(stream, relativeKey, file.ContentType);
+ var storageKey = await FileStorage.SaveObjectAsync(stream, relativeKey, file.ContentType);
var publicUrl = FileStorage.GetPublicUrl(storageKey);
var media = new XhsMedia
diff --git a/src/Oms.Application/Shared/OmsFileUploadHelper.cs b/src/Oms.Application/Shared/OmsFileUploadHelper.cs
index 0c479fc..157cec9 100644
--- a/src/Oms.Application/Shared/OmsFileUploadHelper.cs
+++ b/src/Oms.Application/Shared/OmsFileUploadHelper.cs
@@ -1,12 +1,14 @@
using Fake.Application;
+using Fake.ObjectStorage;
using Microsoft.AspNetCore.Http;
using Oms.Application.Storage;
namespace Oms.Application.Shared;
///
-/// OMS 上传文件辅助(经 IFileStorage,支持本地 / 腾讯云 COS)。
-/// 返回值为 object key,对外展示请再调用 。
+/// OMS 上传文件辅助(经 )。
+/// 返回值为 object key,对外展示请再调用 。
+/// 用户直传请走 Fake.FileManagement(Presign/Sts + Confirm → FileId)。
///
public static class OmsFileUploadHelper
{
@@ -15,27 +17,18 @@ public static class OmsFileUploadHelper
private const long MaxImageBytes = 5 * 1024 * 1024;
- ///
- /// 保存充值截图,返回可访问 URL。
- ///
- public static Task SaveRechargeScreenshotAsync(IFormFile? file, IFileStorage storage) =>
+ public static Task SaveRechargeScreenshotAsync(IFormFile? file, IObjectStorage storage) =>
SaveImageAsync(file, storage, "recharge", "充值截图");
- ///
- /// 保存仓储 SKU 图片。
- ///
- public static Task SaveWarehouseSkuImageAsync(IFormFile file, IFileStorage storage) =>
+ public static Task SaveWarehouseSkuImageAsync(IFormFile file, IObjectStorage storage) =>
SaveImageAsync(file, storage, "sku", "SKU 图片");
- ///
- /// 保存包裹商品图片。
- ///
- public static Task SaveWarehouseParcelImageAsync(IFormFile file, IFileStorage storage) =>
+ public static Task SaveWarehouseParcelImageAsync(IFormFile file, IObjectStorage storage) =>
SaveImageAsync(file, storage, "parcel", "包裹商品图片");
private static async Task SaveImageAsync(
IFormFile? file,
- IFileStorage storage,
+ IObjectStorage storage,
string folder,
string label)
{
@@ -70,7 +63,7 @@ public static class OmsFileUploadHelper
var relativeKey = $"uploads/{folder}/{monthFolder}/{fileName}";
await using var stream = file.OpenReadStream();
- return await storage.SaveAsync(stream, relativeKey, file.ContentType);
+ return await storage.SaveObjectAsync(stream, relativeKey, file.ContentType);
}
private static readonly HashSet ExcelExtensions =
@@ -84,7 +77,7 @@ public static class OmsFileUploadHelper
public static async Task<(string RelativePath, string FileName)> SaveOrderTemplateSampleAsync(
IFormFile file,
int customerId,
- IFileStorage storage)
+ IObjectStorage storage)
{
if (file == null || file.Length <= 0)
{
@@ -106,7 +99,7 @@ public static class OmsFileUploadHelper
var relativeKey = $"uploads/order-templates/{customerId}/{fileName}";
await using var stream = file.OpenReadStream();
- var key = await storage.SaveAsync(
+ var key = await storage.SaveObjectAsync(
stream,
relativeKey,
file.ContentType ?? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
diff --git a/src/Oms.Application/Storage/DirectUploadTickets.cs b/src/Oms.Application/Storage/DirectUploadTickets.cs
deleted file mode 100644
index 7b455c1..0000000
--- a/src/Oms.Application/Storage/DirectUploadTickets.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-namespace Oms.Application.Storage;
-
-///
-/// 预签名 PUT 直传凭证(前端直接 PUT 到 COS,文件不经后端)。
-///
-public sealed class PresignedUploadTicket
-{
- /// 库内应保存的逻辑 key(不含 KeyPrefix)。
- public string Key { get; init; } = string.Empty;
-
- /// 一次性 PUT 地址(含签名)。
- public string UploadUrl { get; init; } = string.Empty;
-
- public string Method { get; init; } = "PUT";
-
- /// PUT 时必须带上的 Content-Type(若签发时绑定了则必填)。
- public string? ContentType { get; init; }
-
- /// PUT 时必须原样携带的请求头。
- public IReadOnlyDictionary Headers { get; init; } =
- new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- public DateTime ExpireAt { get; init; }
-}
-
-///
-/// STS 临时密钥直传凭证(前端用 cos-js-sdk 等上传)。
-///
-public sealed class StsUploadTicket
-{
- /// 库内应保存的逻辑 key。
- public string Key { get; init; } = string.Empty;
-
- /// COS 对象完整键(含 KeyPrefix),SDK putObject 用。
- public string CosKey { get; init; } = string.Empty;
-
- public string Bucket { get; init; } = string.Empty;
-
- public string Region { get; init; } = string.Empty;
-
- public string TmpSecretId { get; init; } = string.Empty;
-
- public string TmpSecretKey { get; init; } = string.Empty;
-
- public string SessionToken { get; init; } = string.Empty;
-
- public long StartTime { get; init; }
-
- public long ExpiredTime { get; init; }
-}
diff --git a/src/Oms.Application/Storage/FileStorageOptions.cs b/src/Oms.Application/Storage/FileStorageOptions.cs
deleted file mode 100644
index 9a4279c..0000000
--- a/src/Oms.Application/Storage/FileStorageOptions.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-namespace Oms.Application.Storage;
-
-///
-/// 文件存储配置。Provider=Local 落盘 wwwroot;Provider=Cos 走腾讯云 COS。
-///
-public class FileStorageOptions
-{
- public const string SectionName = "FileStorage";
-
- ///
- /// 存储提供方:Local | Cos。
- ///
- public string Provider { get; set; } = "Local";
-
- ///
- /// 腾讯云 COS 配置(Provider=Cos 时必填)。
- ///
- public TencentCosOptions Cos { get; set; } = new();
-}
-
-///
-/// 腾讯云 COS 连接参数。
-///
-public class TencentCosOptions
-{
- /// API 密钥 SecretId。
- public string SecretId { get; set; } = string.Empty;
-
- /// API 密钥 SecretKey。
- public string SecretKey { get; set; } = string.Empty;
-
- /// 地域,如 ap-guangzhou。
- public string Region { get; set; } = string.Empty;
-
- /// 存储桶,格式 bucketname-APPID。
- public string Bucket { get; set; } = string.Empty;
-
- ///
- /// 对外访问根地址。可填 CDN / 自定义域名,末尾不要斜杠。
- /// 空则默认 https://{Bucket}.cos.{Region}.myqcloud.com
- ///
- public string? BaseUrl { get; set; }
-
- ///
- /// 对象键前缀(如 oms/prod/),可空。
- ///
- public string KeyPrefix { get; set; } = string.Empty;
-
- ///
- /// 预签名读 URL 有效期(秒)。私有桶临时访问用。
- ///
- public long DurationSecond { get; set; } = 3600;
-
- ///
- /// 预签名上传 / STS 有效期(秒)。建议短于读有效期。
- ///
- public long UploadDurationSecond { get; set; } = 600;
-
- ///
- /// 是否对 GetPublicUrl 返回预签名链接。私有桶必须为 true。
- ///
- public bool SignUrls { get; set; } = true;
-
- /// 是否 HTTPS。
- public bool IsHttps { get; set; } = true;
-}
diff --git a/src/Oms.Application/Storage/FileStorageServiceExtensions.cs b/src/Oms.Application/Storage/FileStorageServiceExtensions.cs
deleted file mode 100644
index fb38e7f..0000000
--- a/src/Oms.Application/Storage/FileStorageServiceExtensions.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-
-namespace Oms.Application.Storage;
-
-public static class FileStorageServiceExtensions
-{
- public static IServiceCollection AddOmsFileStorage(this IServiceCollection services, IConfiguration configuration)
- {
- var section = configuration.GetSection(FileStorageOptions.SectionName);
- services.Configure(section);
-
- var options = section.Get() ?? new FileStorageOptions();
- var provider = ResolveProvider(options);
-
- if (string.Equals(provider, "Cos", StringComparison.OrdinalIgnoreCase))
- {
- services.AddSingleton();
- }
- else
- {
- services.AddSingleton();
- }
-
- services.AddHostedService();
- return services;
- }
-
- ///
- /// 显式 Provider 优先;未写 Provider 但 Cos 凭证齐全时自动走 Cos,避免生产误落 Local。
- ///
- internal static string ResolveProvider(FileStorageOptions options)
- {
- var configured = options.Provider?.Trim();
- if (!string.IsNullOrWhiteSpace(configured))
- {
- return configured;
- }
-
- return HasCompleteCos(options.Cos) ? "Cos" : "Local";
- }
-
- internal static bool HasCompleteCos(TencentCosOptions? cos) =>
- cos != null
- && !string.IsNullOrWhiteSpace(cos.SecretId)
- && !string.IsNullOrWhiteSpace(cos.SecretKey)
- && !string.IsNullOrWhiteSpace(cos.Region)
- && !string.IsNullOrWhiteSpace(cos.Bucket)
- && !cos.Bucket.Contains("your-bucket", StringComparison.OrdinalIgnoreCase);
-}
-
-///
-/// 启动时打印实际 IFileStorage 实现,便于核对 Consul 是否生效。
-///
-internal sealed class FileStorageStartupLogger(
- IFileStorage storage,
- IOptions options,
- ILogger logger) : IHostedService
-{
- public Task StartAsync(CancellationToken cancellationToken)
- {
- var cfg = options.Value;
- var cosReady = FileStorageServiceExtensions.HasCompleteCos(cfg.Cos);
- logger.LogInformation(
- "IFileStorage 实际类型={Implementation},配置 Provider={ConfiguredProvider},Cos凭证齐全={CosReady},Section={Section}",
- storage.GetType().FullName,
- string.IsNullOrWhiteSpace(cfg.Provider) ? "(empty)" : cfg.Provider,
- cosReady,
- FileStorageOptions.SectionName);
-
- if (storage is LocalFileStorage && cosReady)
- {
- logger.LogWarning(
- "已检测到齐全 Cos 凭证,但仍在使用 LocalFileStorage。请确认 Consul 中 {Section}:Provider 是否为 Cos(当前={ConfiguredProvider})",
- FileStorageOptions.SectionName,
- string.IsNullOrWhiteSpace(cfg.Provider) ? "(empty)" : cfg.Provider);
- }
-
- if (storage is LocalFileStorage && !cosReady)
- {
- logger.LogWarning(
- "当前为 Local 存储。生产若要用 Cos,请在 Consul KV(idwms/admin)配置 {Section}:Provider=Cos 及 Cos.SecretId/SecretKey/Region/Bucket",
- FileStorageOptions.SectionName);
- }
-
- return Task.CompletedTask;
- }
-
- public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-}
diff --git a/src/Oms.Application/Storage/FileStorageUploadPaths.cs b/src/Oms.Application/Storage/FileStorageUploadPaths.cs
deleted file mode 100644
index 5410a72..0000000
--- a/src/Oms.Application/Storage/FileStorageUploadPaths.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Fake.Application;
-
-namespace Oms.Application.Storage;
-
-///
-/// 直传目录与 object key 生成。
-///
-public static class FileStorageUploadPaths
-{
- private static readonly HashSet AllowedFolders = new(StringComparer.OrdinalIgnoreCase)
- {
- "avatars",
- "sku",
- "parcel",
- "recharge",
- "order-files",
- "order-templates",
- "xhs",
- "prints",
- "exports",
- "misc"
- };
-
- public static string NormalizeFolder(string? folder)
- {
- var value = (folder ?? string.Empty).Trim().Trim('/').Replace('\\', '/');
- if (string.IsNullOrWhiteSpace(value))
- {
- throw new BusinessException("上传目录 folder 不能为空");
- }
-
- // 允许 uploads/sku 或 sku
- if (value.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase))
- {
- value = value["uploads/".Length..];
- }
-
- var root = value.Split('/', StringSplitOptions.RemoveEmptyEntries)[0];
- if (!AllowedFolders.Contains(root))
- {
- throw new BusinessException($"不支持的上传目录: {root}");
- }
-
- return value;
- }
-
- public static string BuildLogicalKey(string folder, string fileName)
- {
- var normalizedFolder = NormalizeFolder(folder);
- var ext = Path.GetExtension(fileName);
- if (string.IsNullOrWhiteSpace(ext) || ext.Length > 16)
- {
- ext = ".bin";
- }
-
- var month = DateTime.Now.ToString("yyyyMM");
- return $"uploads/{normalizedFolder}/{month}/{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
- }
-}
diff --git a/src/Oms.Application/Storage/IFileStorage.cs b/src/Oms.Application/Storage/IFileStorage.cs
deleted file mode 100644
index cd6b501..0000000
--- a/src/Oms.Application/Storage/IFileStorage.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-namespace Oms.Application.Storage;
-
-///
-/// 统一文件存储(本地 / 腾讯云 COS)。
-/// 约定:库内只存 object key(如 uploads/avatars/a.jpg)。
-/// 私有桶场景:访问用临时签名 URL;用户上传用预签名 PUT 或 STS,文件不经业务服务器。
-///
-public interface IFileStorage
-{
- /// 是否支持前端直传(COS=true,Local=false)。
- bool SupportsClientDirectUpload { get; }
-
- ///
- /// 服务端落盘/上传(仅用于后端生成的文件,如面单 PDF、导出)。用户上传请走直传。
- ///
- Task SaveAsync(
- Stream content,
- string relativeKey,
- string? contentType = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// 临时访问地址。Cos 私有桶返回预签名 GET;Local 返回 /uploads/...;外链原样返回。
- ///
- string GetPublicUrl(string? keyOrUrl);
-
- ///
- /// 签发前端直传用的预签名 PUT URL。
- ///
- Task CreatePresignedUploadAsync(
- string folder,
- string fileName,
- string? contentType = null,
- CancellationToken cancellationToken = default);
-
- ///
- /// 签发 STS 临时密钥(前端 COS SDK 直传)。不支持时抛业务异常。
- ///
- Task CreateStsUploadAsync(
- string folder,
- string fileName,
- CancellationToken cancellationToken = default);
-
- ///
- /// 将 key / 相对路径 / 完整 URL 规范为库内应保存的 object key。
- ///
- string? ToStorageKey(string? keyOrUrl);
-
- Task OpenReadAsync(string pathOrUrl, CancellationToken cancellationToken = default);
-
- Task ExistsAsync(string pathOrUrl, CancellationToken cancellationToken = default);
-
- Task DeleteAsync(string pathOrUrl, CancellationToken cancellationToken = default);
-}
diff --git a/src/Oms.Application/Storage/LocalFileStorage.cs b/src/Oms.Application/Storage/LocalFileStorage.cs
deleted file mode 100644
index b9a54c0..0000000
--- a/src/Oms.Application/Storage/LocalFileStorage.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using Fake.Application;
-using Microsoft.AspNetCore.Hosting;
-
-namespace Oms.Application.Storage;
-
-///
-/// 本地 wwwroot 文件存储。库内存 key,对外为 /key。不支持前端直传 COS。
-///
-public sealed class LocalFileStorage(IWebHostEnvironment environment) : IFileStorage
-{
- public bool SupportsClientDirectUpload => false;
-
- public async Task SaveAsync(
- Stream content,
- string relativeKey,
- string? contentType = null,
- CancellationToken cancellationToken = default)
- {
- var key = FileStoragePath.NormalizeKey(relativeKey);
- var absolutePath = ToAbsolutePath(key);
- var directory = Path.GetDirectoryName(absolutePath);
- if (!string.IsNullOrWhiteSpace(directory))
- {
- try
- {
- Directory.CreateDirectory(directory);
- }
- catch (UnauthorizedAccessException ex)
- {
- throw new UnauthorizedAccessException(
- $"本地文件存储无法写入目录 '{directory}'(WebRoot={GetWebRoot()})。" +
- "容器请确保 wwwroot/uploads 可写,或将 Oms:FileStorage:Provider 设为 Cos。",
- ex);
- }
- catch (IOException ex)
- {
- throw new IOException(
- $"本地文件存储无法创建目录 '{directory}'(WebRoot={GetWebRoot()})。" +
- "容器请确保 wwwroot/uploads 可写,或将 Oms:FileStorage:Provider 设为 Cos。",
- ex);
- }
- }
-
- await using var file = new FileStream(
- absolutePath,
- FileMode.Create,
- FileAccess.Write,
- FileShare.None,
- bufferSize: 81920,
- useAsync: true);
- if (content.CanSeek)
- {
- content.Position = 0;
- }
-
- await content.CopyToAsync(file, cancellationToken);
- return key;
- }
-
- public string GetPublicUrl(string? keyOrUrl)
- {
- if (string.IsNullOrWhiteSpace(keyOrUrl) || keyOrUrl == "0")
- {
- return keyOrUrl ?? string.Empty;
- }
-
- if (FileStoragePath.IsAbsoluteUrl(keyOrUrl))
- {
- return keyOrUrl;
- }
-
- if (keyOrUrl.StartsWith('/'))
- {
- return keyOrUrl.Replace('\\', '/');
- }
-
- return "/" + FileStoragePath.NormalizeKey(keyOrUrl);
- }
-
- public Task CreatePresignedUploadAsync(
- string folder,
- string fileName,
- string? contentType = null,
- CancellationToken cancellationToken = default) =>
- throw new BusinessException(
- "当前为 Local 存储,不支持 COS 直传。请将 Oms:FileStorage:Provider 设为 Cos,或继续使用服务端 multipart 上传。");
-
- public Task CreateStsUploadAsync(
- string folder,
- string fileName,
- CancellationToken cancellationToken = default) =>
- throw new BusinessException("当前为 Local 存储,不支持 STS 直传。请将 Oms:FileStorage:Provider 设为 Cos。");
-
- public string? ToStorageKey(string? keyOrUrl)
- {
- if (string.IsNullOrWhiteSpace(keyOrUrl))
- {
- return null;
- }
-
- if (keyOrUrl == "0")
- {
- return "0";
- }
-
- if (FileStoragePath.IsAbsoluteUrl(keyOrUrl))
- {
- return keyOrUrl.Trim();
- }
-
- return FileStoragePath.NormalizeKey(keyOrUrl);
- }
-
- public Task OpenReadAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- var absolutePath = ResolveAbsolutePath(pathOrUrl);
- if (!File.Exists(absolutePath))
- {
- throw new FileNotFoundException("本地文件不存在", absolutePath);
- }
-
- Stream stream = new FileStream(
- absolutePath,
- FileMode.Open,
- FileAccess.Read,
- FileShare.Read,
- bufferSize: 81920,
- useAsync: true);
- return Task.FromResult(stream);
- }
-
- public Task ExistsAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- var absolutePath = ResolveAbsolutePath(pathOrUrl);
- return Task.FromResult(File.Exists(absolutePath));
- }
-
- public Task DeleteAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- var absolutePath = ResolveAbsolutePath(pathOrUrl);
- if (File.Exists(absolutePath))
- {
- File.Delete(absolutePath);
- }
-
- return Task.CompletedTask;
- }
-
- private string ResolveAbsolutePath(string pathOrUrl)
- {
- if (string.IsNullOrWhiteSpace(pathOrUrl))
- {
- throw new ArgumentException("路径不能为空", nameof(pathOrUrl));
- }
-
- if (Path.IsPathRooted(pathOrUrl) && File.Exists(pathOrUrl))
- {
- return pathOrUrl;
- }
-
- return ToAbsolutePath(FileStoragePath.NormalizeKey(pathOrUrl));
- }
-
- private string ToAbsolutePath(string relativeKey) =>
- Path.Combine(GetWebRoot(), relativeKey.Replace('/', Path.DirectorySeparatorChar));
-
- private string GetWebRoot() =>
- string.IsNullOrWhiteSpace(environment.WebRootPath)
- ? Path.Combine(environment.ContentRootPath, "wwwroot")
- : environment.WebRootPath;
-}
diff --git a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
new file mode 100644
index 0000000..ab34314
--- /dev/null
+++ b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
@@ -0,0 +1,86 @@
+using Fake.ObjectStorage;
+
+namespace Oms.Application.Storage;
+
+///
+/// OMS 对 的约定封装:库内存逻辑 object key(或外链原样),展示用临时/公开 URL。
+///
+public static class OmsObjectStorageExtensions
+{
+ ///
+ /// 将 key / 相对路径规范为库内 object key;绝对外链原样返回;空返回 null。
+ ///
+ public static string? ToStorageKey(string? keyOrUrl)
+ {
+ if (string.IsNullOrWhiteSpace(keyOrUrl))
+ {
+ return null;
+ }
+
+ var raw = keyOrUrl.Trim();
+ if (raw == "0")
+ {
+ return "0";
+ }
+
+ if (FileStoragePath.IsAbsoluteUrl(raw))
+ {
+ return raw;
+ }
+
+ return FileStoragePath.NormalizeKey(raw);
+ }
+
+ ///
+ /// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 。
+ ///
+ public static async Task GetPublicUrlAsync(
+ this IObjectStorage storage,
+ string? keyOrUrl,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(keyOrUrl) || keyOrUrl == "0")
+ {
+ return string.Empty;
+ }
+
+ var raw = keyOrUrl.Trim();
+ if (FileStoragePath.IsAbsoluteUrl(raw))
+ {
+ return raw;
+ }
+
+ var key = FileStoragePath.NormalizeKey(raw);
+ return await storage.GetUrlAsync(key, cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// 同步取访问 URL(映射 DTO 等场景)。
+ ///
+ public static string GetPublicUrl(this IObjectStorage storage, string? keyOrUrl) =>
+ storage.GetPublicUrlAsync(keyOrUrl).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ ///
+ /// 服务端写入对象,返回逻辑 object key。
+ ///
+ public static async Task SaveObjectAsync(
+ this IObjectStorage storage,
+ Stream content,
+ string relativeKey,
+ string? contentType = null,
+ CancellationToken cancellationToken = default)
+ {
+ var result = await storage.SaveAsync(
+ new ObjectStorageSaveArgs
+ {
+ ObjectKey = FileStoragePath.NormalizeKey(relativeKey),
+ Content = content,
+ ContentType = contentType
+ },
+ cancellationToken);
+ return result.ObjectKey;
+ }
+
+ public static bool IsLocalProvider(this IObjectStorage storage) =>
+ string.Equals(storage.Name, "Local", StringComparison.OrdinalIgnoreCase);
+}
diff --git a/src/Oms.Application/Storage/TencentCosFileStorage.cs b/src/Oms.Application/Storage/TencentCosFileStorage.cs
deleted file mode 100644
index 9f152d7..0000000
--- a/src/Oms.Application/Storage/TencentCosFileStorage.cs
+++ /dev/null
@@ -1,487 +0,0 @@
-using COSSTS;
-using COSXML;
-using COSXML.Auth;
-using COSXML.Model.Object;
-using COSXML.Model.Tag;
-using Fake.Application;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace Oms.Application.Storage;
-
-///
-/// 腾讯云 COS:库内存逻辑 key;读返回预签名 GET;写支持预签名 PUT / STS 前端直传。
-///
-public sealed class TencentCosFileStorage : IFileStorage, IDisposable
-{
- private static readonly string[] UploadActions =
- [
- "name/cos:PutObject",
- "name/cos:PostObject",
- "name/cos:InitiateMultipartUpload",
- "name/cos:ListMultipartUploads",
- "name/cos:ListParts",
- "name/cos:UploadPart",
- "name/cos:CompleteMultipartUpload",
- "name/cos:AbortMultipartUpload"
- ];
-
- private readonly TencentCosOptions _options;
- private readonly CosXmlServer _cosXml;
- private readonly ILogger _logger;
- private readonly string _publicBaseUrl;
- private readonly string _keyPrefix;
- private readonly string _appId;
- private readonly string? _customHost;
-
- public TencentCosFileStorage(
- IOptions options,
- ILogger logger)
- {
- _logger = logger;
- _options = options.Value.Cos ?? new TencentCosOptions();
- ValidateOptions(_options);
-
- _appId = ExtractAppId(_options.Bucket);
- var config = new CosXmlConfig.Builder()
- .IsHttps(_options.IsHttps)
- .SetRegion(_options.Region)
- .SetAppid(_appId)
- .Build();
-
- var signSeconds = Math.Max(60, Math.Max(_options.DurationSecond, _options.UploadDurationSecond));
- var credential = new DefaultQCloudCredentialProvider(
- _options.SecretId,
- _options.SecretKey,
- signSeconds);
-
- _cosXml = new CosXmlServer(config, credential);
- _publicBaseUrl = ResolvePublicBaseUrl(_options);
- _keyPrefix = (_options.KeyPrefix ?? string.Empty).Replace('\\', '/').Trim('/');
- _customHost = Uri.TryCreate(_publicBaseUrl, UriKind.Absolute, out var baseUri)
- ? baseUri.Host
- : null;
- }
-
- public bool SupportsClientDirectUpload => true;
-
- public Task SaveAsync(
- Stream content,
- string relativeKey,
- string? contentType = null,
- CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var logicalKey = FileStoragePath.NormalizeKey(relativeKey);
- var objectKey = ToObjectKey(logicalKey);
- if (content.CanSeek)
- {
- content.Position = 0;
- }
-
- Stream uploadStream = content;
- MemoryStream? owned = null;
- if (!content.CanSeek)
- {
- owned = new MemoryStream();
- content.CopyTo(owned);
- owned.Position = 0;
- uploadStream = owned;
- }
-
- try
- {
- var request = new PutObjectRequest(_options.Bucket, objectKey, uploadStream);
- if (!string.IsNullOrWhiteSpace(contentType))
- {
- request.SetRequestHeader("Content-Type", contentType);
- }
-
- var result = _cosXml.PutObject(request);
- if (result.httpCode is < 200 or >= 300)
- {
- throw new BusinessException($"上传 COS 失败: HTTP {result.httpCode}");
- }
-
- _logger.LogDebug("COS 服务端上传成功: {ObjectKey}", objectKey);
- return Task.FromResult(logicalKey);
- }
- finally
- {
- owned?.Dispose();
- }
- }
-
- public string GetPublicUrl(string? keyOrUrl)
- {
- if (string.IsNullOrWhiteSpace(keyOrUrl) || keyOrUrl == "0")
- {
- return keyOrUrl ?? string.Empty;
- }
-
- if (FileStoragePath.IsAbsoluteUrl(keyOrUrl))
- {
- if (!IsManagedPublicUrl(keyOrUrl))
- {
- return keyOrUrl;
- }
-
- // 已是签名 URL:原样返回,避免重复签名
- if (keyOrUrl.Contains("q-signature=", StringComparison.OrdinalIgnoreCase)
- || keyOrUrl.Contains("sign=", StringComparison.OrdinalIgnoreCase))
- {
- return keyOrUrl;
- }
-
- var logical = StripKeyPrefix(FileStoragePath.NormalizeKey(keyOrUrl));
- return BuildSignedGetUrl(logical);
- }
-
- return BuildSignedGetUrl(keyOrUrl);
- }
-
- public Task CreatePresignedUploadAsync(
- string folder,
- string fileName,
- string? contentType = null,
- CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var logicalKey = FileStorageUploadPaths.BuildLogicalKey(folder, fileName);
- var objectKey = ToObjectKey(logicalKey);
- var duration = Math.Max(60, _options.UploadDurationSecond);
- var headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
- if (!string.IsNullOrWhiteSpace(contentType))
- {
- headers["Content-Type"] = contentType.Trim();
- }
-
- var preSign = new PreSignatureStruct
- {
- appid = _appId,
- bucket = _options.Bucket,
- region = _options.Region,
- key = objectKey,
- httpMethod = "PUT",
- isHttps = _options.IsHttps,
- signDurationSecond = duration,
- headers = headers.Count > 0 ? headers : null,
- queryParameters = null
- };
- ApplyCustomHost(preSign);
-
- var uploadUrl = _cosXml.GenerateSignURL(preSign);
- return Task.FromResult(new PresignedUploadTicket
- {
- Key = logicalKey,
- UploadUrl = uploadUrl,
- Method = "PUT",
- ContentType = string.IsNullOrWhiteSpace(contentType) ? null : contentType.Trim(),
- Headers = headers,
- ExpireAt = DateTime.UtcNow.AddSeconds(duration)
- });
- }
-
- public Task CreateStsUploadAsync(
- string folder,
- string fileName,
- CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- var logicalKey = FileStorageUploadPaths.BuildLogicalKey(folder, fileName);
- var objectKey = ToObjectKey(logicalKey);
- var duration = (int)Math.Max(60, _options.UploadDurationSecond);
-
- var values = new Dictionary
- {
- ["bucket"] = _options.Bucket,
- ["region"] = _options.Region,
- ["allowPrefix"] = objectKey,
- ["allowActions"] = UploadActions,
- ["durationSeconds"] = duration,
- ["secretId"] = _options.SecretId,
- ["secretKey"] = _options.SecretKey
- };
-
- Dictionary credential;
- try
- {
- credential = STSClient.genCredential(values);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "申请 COS STS 临时密钥失败");
- throw new BusinessException("申请 COS 临时上传凭证失败,请检查 SecretId/SecretKey 与 CAM 权限");
- }
-
- var credentials = ExtractCredentials(credential["Credentials"]);
- var startTime = ToUnixSeconds(credential.GetValueOrDefault("StartTime"));
- var expiredTime = ToUnixSeconds(credential.GetValueOrDefault("ExpiredTime"));
- if (startTime <= 0)
- {
- startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- }
-
- if (expiredTime <= 0)
- {
- expiredTime = startTime + duration;
- }
-
- return Task.FromResult(new StsUploadTicket
- {
- Key = logicalKey,
- CosKey = objectKey,
- Bucket = _options.Bucket,
- Region = _options.Region,
- TmpSecretId = credentials.tmpSecretId,
- TmpSecretKey = credentials.tmpSecretKey,
- SessionToken = credentials.token,
- StartTime = startTime,
- ExpiredTime = expiredTime
- });
- }
-
- public string? ToStorageKey(string? keyOrUrl)
- {
- if (string.IsNullOrWhiteSpace(keyOrUrl))
- {
- return null;
- }
-
- if (keyOrUrl == "0")
- {
- return "0";
- }
-
- if (FileStoragePath.IsAbsoluteUrl(keyOrUrl))
- {
- if (!IsManagedPublicUrl(keyOrUrl))
- {
- return keyOrUrl.Trim();
- }
-
- var pathKey = FileStoragePath.NormalizeKey(keyOrUrl);
- return StripKeyPrefix(pathKey);
- }
-
- return FileStoragePath.NormalizeKey(keyOrUrl);
- }
-
- public Task OpenReadAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
- var objectKey = ToObjectKey(pathOrUrl);
- var request = new GetObjectBytesRequest(_options.Bucket, objectKey);
- var result = _cosXml.GetObject(request);
- if (result.httpCode is < 200 or >= 300)
- {
- throw new FileNotFoundException($"COS 对象不存在或读取失败: {objectKey}");
- }
-
- Stream stream = new MemoryStream(result.content);
- stream.Position = 0;
- return Task.FromResult(stream);
- }
-
- public Task ExistsAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
- try
- {
- var objectKey = ToObjectKey(pathOrUrl);
- var request = new HeadObjectRequest(_options.Bucket, objectKey);
- var result = _cosXml.HeadObject(request);
- return Task.FromResult(result.httpCode is >= 200 and < 300);
- }
- catch
- {
- return Task.FromResult(false);
- }
- }
-
- public Task DeleteAsync(string pathOrUrl, CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
- try
- {
- var objectKey = ToObjectKey(pathOrUrl);
- var request = new DeleteObjectRequest(_options.Bucket, objectKey);
- _cosXml.DeleteObject(request);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "删除 COS 对象失败: {Path}", pathOrUrl);
- }
-
- return Task.CompletedTask;
- }
-
- public void Dispose()
- {
- }
-
- private string BuildSignedGetUrl(string logicalOrRelativeKey)
- {
- var objectKey = ToObjectKey(logicalOrRelativeKey);
- if (!_options.SignUrls)
- {
- _logger.LogWarning(
- "Cos.SignUrls=false,私有桶下浏览器将无法直接打开对象。建议保持 SignUrls=true。ObjectKey={ObjectKey}",
- objectKey);
- return $"{_publicBaseUrl}/{objectKey}";
- }
-
- try
- {
- var preSign = new PreSignatureStruct
- {
- appid = _appId,
- bucket = _options.Bucket,
- region = _options.Region,
- key = objectKey,
- httpMethod = "GET",
- isHttps = _options.IsHttps,
- signDurationSecond = Math.Max(60, _options.DurationSecond),
- headers = null,
- queryParameters = null
- };
- ApplyCustomHost(preSign);
- return _cosXml.GenerateSignURL(preSign);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "生成 COS 预签名 GET 失败: {ObjectKey}", objectKey);
- throw new BusinessException("生成临时访问地址失败");
- }
- }
-
- private void ApplyCustomHost(PreSignatureStruct preSign)
- {
- if (!string.IsNullOrWhiteSpace(_customHost)
- && !_customHost.EndsWith(".myqcloud.com", StringComparison.OrdinalIgnoreCase))
- {
- preSign.host = _customHost;
- preSign.signHost = true;
- }
- }
-
- private string ToObjectKey(string pathOrUrl)
- {
- var key = FileStoragePath.NormalizeKey(pathOrUrl);
- if (!string.IsNullOrEmpty(_keyPrefix)
- && !key.StartsWith(_keyPrefix + "/", StringComparison.OrdinalIgnoreCase)
- && !string.Equals(key, _keyPrefix, StringComparison.OrdinalIgnoreCase))
- {
- key = $"{_keyPrefix}/{key}";
- }
-
- return key;
- }
-
- private string StripKeyPrefix(string objectOrLogicalKey)
- {
- if (string.IsNullOrEmpty(_keyPrefix))
- {
- return objectOrLogicalKey;
- }
-
- if (objectOrLogicalKey.StartsWith(_keyPrefix + "/", StringComparison.OrdinalIgnoreCase))
- {
- return objectOrLogicalKey[(_keyPrefix.Length + 1)..];
- }
-
- if (string.Equals(objectOrLogicalKey, _keyPrefix, StringComparison.OrdinalIgnoreCase))
- {
- return string.Empty;
- }
-
- return objectOrLogicalKey;
- }
-
- private bool IsManagedPublicUrl(string url)
- {
- if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
- {
- return false;
- }
-
- if (Uri.TryCreate(_publicBaseUrl, UriKind.Absolute, out var baseUri)
- && string.Equals(uri.Host, baseUri.Host, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- var defaultHost = $"{_options.Bucket}.cos.{_options.Region}.myqcloud.com";
- return string.Equals(uri.Host, defaultHost, StringComparison.OrdinalIgnoreCase);
- }
-
- private static (string tmpSecretId, string tmpSecretKey, string token) ExtractCredentials(object? raw)
- {
- if (raw == null)
- {
- throw new BusinessException("STS 返回 Credentials 为空");
- }
-
- var json = JsonConvert.SerializeObject(raw);
- var obj = JObject.Parse(json);
- var id = obj.Value("TmpSecretId") ?? obj.Value("tmpSecretId");
- var key = obj.Value("TmpSecretKey") ?? obj.Value("tmpSecretKey");
- var token = obj.Value("Token") ?? obj.Value("sessionToken") ?? obj.Value("token");
- if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(token))
- {
- throw new BusinessException("STS 凭证字段不完整");
- }
-
- return (id, key, token);
- }
-
- private static long ToUnixSeconds(object? value) =>
- value switch
- {
- null => 0,
- long l => l,
- int i => i,
- string s when long.TryParse(s, out var n) => n,
- JValue j when j.Type == JTokenType.Integer => j.Value(),
- _ => 0
- };
-
- private static string ResolvePublicBaseUrl(TencentCosOptions options)
- {
- if (!string.IsNullOrWhiteSpace(options.BaseUrl))
- {
- return options.BaseUrl.Trim().TrimEnd('/');
- }
-
- var scheme = options.IsHttps ? "https" : "http";
- return $"{scheme}://{options.Bucket}.cos.{options.Region}.myqcloud.com";
- }
-
- private static string ExtractAppId(string bucket)
- {
- var idx = bucket.LastIndexOf('-');
- if (idx <= 0 || idx >= bucket.Length - 1)
- {
- throw new BusinessException("COS Bucket 格式应为 bucketname-APPID");
- }
-
- return bucket[(idx + 1)..];
- }
-
- private static void ValidateOptions(TencentCosOptions options)
- {
- if (string.IsNullOrWhiteSpace(options.SecretId)
- || string.IsNullOrWhiteSpace(options.SecretKey)
- || string.IsNullOrWhiteSpace(options.Region)
- || string.IsNullOrWhiteSpace(options.Bucket))
- {
- throw new BusinessException(
- "Oms:FileStorage:Cos 配置不完整,请填写 SecretId / SecretKey / Region / Bucket");
- }
- }
-}
diff --git a/src/SimpleAdmin.Api/Auth/AuthService.cs b/src/SimpleAdmin.Api/Auth/AuthService.cs
index 6cc1886..632f352 100644
--- a/src/SimpleAdmin.Api/Auth/AuthService.cs
+++ b/src/SimpleAdmin.Api/Auth/AuthService.cs
@@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
+using Fake.ObjectStorage;
using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.CustomersAggregate.Entities;
@@ -43,7 +44,7 @@ public partial class AuthService(
IWeChatLoginSessionStore weChatLoginSessionStore,
IMemoryCache memoryCache,
IRbacAuthCache rbacAuthCache,
- IFileStorage fileStorage)
+ IObjectStorage fileStorage)
: ApplicationService
{
private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3);
@@ -418,7 +419,7 @@ public partial class AuthService(
}
output.Position = 0;
- var avatarKey = await fileStorage.SaveAsync(output, relativeKey, "image/jpeg", cancellationToken);
+ var avatarKey = await fileStorage.SaveObjectAsync(output, relativeKey, "image/jpeg", cancellationToken);
var user = await userRepository.FirstAsync(u => u.Id == userId, cancellationToken: cancellationToken);
user.UpdateAvatar(avatarKey);
await userRepository.UpdateAsync(user, cancellationToken: cancellationToken);
diff --git a/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj b/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
index 2648d19..0143f14 100644
--- a/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
+++ b/src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
@@ -15,6 +15,9 @@
+
+
+
diff --git a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
index bf93dc0..0d0e2a1 100644
--- a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
+++ b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs
@@ -8,15 +8,20 @@ using Fake.AspNetCore.ExceptionHandling;
using Fake.AspNetCore.Newtonsoft;
using Fake.AspNetCore.Swagger;
using Fake.FeiShu;
+using Fake.FileManagement.Application;
+using Fake.FileManagement.Infrastructure;
+using Fake.ObjectStorage;
using Fake.Security.Claims;
using Fake.Serilog;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Authorization;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Oms.Application;
using Oms.SqlSugarCore;
using SimpleAdmin.Api.Auth;
+using SimpleAdmin.Api.Storage;
using SimpleAdmin.Api.Swagger;
using SimpleAdmin.Api.WeChat;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -29,6 +34,9 @@ namespace SimpleAdmin.Api;
typeof(OmsSqlSugarCoreModule),
typeof(FakeRbacApplicationModule),
typeof(FakeRbacInfrastructureModule),
+ typeof(FakeFileManagementApplicationModule),
+ typeof(FakeFileManagementInfrastructureModule),
+ typeof(FakeObjectStorageTencentCosModule),
typeof(FakeAspNetCoreModule),
typeof(FakeSerilogModule)
)]
@@ -46,7 +54,7 @@ public class SimpleAdminApiModule : FakeModule
var services = context.Services;
var configuration = context.Services.GetConfiguration();
- // 配置动态 API - 扫描 OMS 与 RBAC 应用服务
+ // 配置动态 API - 扫描 OMS / RBAC / FileManagement 应用服务
services.Configure(options =>
{
options.ApplicationServices2Controller();
@@ -63,6 +71,10 @@ public class SimpleAdminApiModule : FakeModule
type != typeof(Fake.Rbac.Application.Services.AuthService) &&
type != typeof(Fake.Rbac.Application.Services.UserService);
});
+ options.ApplicationServices2Controller(settings =>
+ {
+ settings.RootPath = "files";
+ });
});
services.AddOmsWeChat(configuration);
@@ -128,6 +140,8 @@ public class SimpleAdminApiModule : FakeModule
});
// 添加授权服务
services.AddAuthorization();
+
+ services.AddHostedService();
}
public override void ConfigureApplication(ApplicationConfigureContext context)
@@ -162,6 +176,13 @@ public class SimpleAdminApiModule : FakeModule
// 端点映射
app.MapControllers();
+
+ // FileManagement 与 RBAC 同库并行(EF);应用 Pending migrations 建 stored_file
+ using (var scope = app.Services.CreateScope())
+ {
+ var fileDb = scope.ServiceProvider.GetRequiredService();
+ fileDb.Database.Migrate();
+ }
app.Services.GetRequiredService().Send("Application started");
}
diff --git a/src/SimpleAdmin.Api/Storage/ObjectStorageStartupLogger.cs b/src/SimpleAdmin.Api/Storage/ObjectStorageStartupLogger.cs
new file mode 100644
index 0000000..90a61bc
--- /dev/null
+++ b/src/SimpleAdmin.Api/Storage/ObjectStorageStartupLogger.cs
@@ -0,0 +1,36 @@
+using Fake.ObjectStorage;
+using Microsoft.Extensions.Options;
+
+namespace SimpleAdmin.Api.Storage;
+
+///
+/// 启动时打印实际 IObjectStorage 实现,便于核对 Consul ObjectStorage 配置是否生效。
+///
+internal sealed class ObjectStorageStartupLogger(
+ IObjectStorage storage,
+ IOptions options,
+ ILogger logger) : IHostedService
+{
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ var cfg = options.Value;
+ logger.LogInformation(
+ "IObjectStorage 实际类型={Implementation},Name={Name},配置 Provider={ConfiguredProvider},SignUrlsByDefault={SignUrlsByDefault},KeyPrefix={KeyPrefix}",
+ storage.GetType().FullName,
+ storage.Name,
+ string.IsNullOrWhiteSpace(cfg.Provider) ? "(empty)" : cfg.Provider,
+ cfg.SignUrlsByDefault,
+ string.IsNullOrWhiteSpace(cfg.KeyPrefix) ? "(empty)" : cfg.KeyPrefix);
+
+ if (string.Equals(storage.Name, "Local", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(cfg.Provider, "TencentCos", StringComparison.OrdinalIgnoreCase))
+ {
+ logger.LogWarning(
+ "配置 Provider=TencentCos,但仍在使用 Local。请确认已引用 Fake.ObjectStorage.TencentCos 且 Consul 中 ObjectStorage:Provider 已生效");
+ }
+
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/src/SimpleAdmin.Api/appsettings.Development.json b/src/SimpleAdmin.Api/appsettings.Development.json
index 987adaf..1b28af5 100644
--- a/src/SimpleAdmin.Api/appsettings.Development.json
+++ b/src/SimpleAdmin.Api/appsettings.Development.json
@@ -59,22 +59,34 @@
"FollowUrl": ""
}
},
- "Oms": {
- "FileStorage": {
- "Provider": "Local",
- "Cos": {
- "SecretId": "",
- "SecretKey": "",
- "Region": "ap-guangzhou",
- "Bucket": "your-bucket-1250000000",
- "BaseUrl": "",
- "KeyPrefix": "",
- "DurationSecond": 3600,
- "UploadDurationSecond": 600,
- "SignUrls": true,
- "IsHttps": true
- }
+ "ObjectStorage": {
+ "Provider": "Local",
+ "KeyPrefix": "",
+ "SignUrlsByDefault": true,
+ "DefaultSignedUrlExpiresSeconds": 3600,
+ "DefaultUploadExpiresSeconds": 600,
+ "Local": {
+ "BasePath": "wwwroot",
+ "BaseUrl": ""
},
+ "TencentCos": {
+ "SecretId": "",
+ "SecretKey": "",
+ "Region": "ap-guangzhou",
+ "Bucket": "your-bucket-1250000000",
+ "BaseUrl": "",
+ "SignUrls": true,
+ "UseHttps": true,
+ "DurationSecond": 600
+ }
+ },
+ "FileManagement": {
+ "SignUrlsByDefault": true,
+ "DefaultSignedUrlExpiresSeconds": 3600,
+ "DefaultMaxSizeBytes": 20971520,
+ "DefaultStorageSource": "default"
+ },
+ "Oms": {
"TrackService": {
"Enabled": true,
"BaseUrl": "http://api.nanafa.com/track",