Compare commits
5 Commits
0a16f27664
...
943ba539a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 943ba539a4 | |||
| 23eb80931a | |||
| c886e8cef3 | |||
| 8f7a4c1405 | |||
| 93bef462fb |
@ -1,8 +1,8 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
@ -22,7 +22,7 @@ public sealed class LabelPdfGenerationService(
|
||||
ISystemOtherConfigRepository systemOtherConfigRepository,
|
||||
IOrderRepository orderRepository,
|
||||
OrderFileManager orderFileManager,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IFileStorage fileStorage,
|
||||
ILogger<LabelPdfGenerationService> logger)
|
||||
{
|
||||
public async Task<LabelPdfGenerateResult> GenerateAndPersistAsync(
|
||||
@ -158,11 +158,6 @@ public sealed class LabelPdfGenerationService(
|
||||
|
||||
private async Task<string> SavePdfAsync(string serviceNo, byte[] pdfBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
var webRoot = string.IsNullOrWhiteSpace(webHostEnvironment.WebRootPath)
|
||||
? Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot")
|
||||
: webHostEnvironment.WebRootPath;
|
||||
var directory = Path.Combine(webRoot, "uploads", "prints", "orders");
|
||||
Directory.CreateDirectory(directory);
|
||||
var safeNo = string.IsNullOrWhiteSpace(serviceNo) ? "order" : serviceNo.Trim();
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
@ -170,9 +165,9 @@ public sealed class LabelPdfGenerationService(
|
||||
}
|
||||
|
||||
var fileName = $"{safeNo}-{DateTime.Now:yyyyMMddHHmmss}.pdf";
|
||||
var absolute = Path.Combine(directory, fileName);
|
||||
await File.WriteAllBytesAsync(absolute, pdfBytes, cancellationToken);
|
||||
return $"/uploads/prints/orders/{fileName}";
|
||||
var relativeKey = $"uploads/prints/orders/{fileName}";
|
||||
await using var stream = new MemoryStream(pdfBytes);
|
||||
return await fileStorage.SaveAsync(stream, relativeKey, "application/pdf", cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
<PackageReference Include="MiniExcel" Version="1.43.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.0" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.49" />
|
||||
<PackageReference Include="ZXing.Net" Version="0.16.10" />
|
||||
<PackageReference Include="ZXing.Net.Bindings.ImageSharp.V3" Version="0.16.15" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -13,6 +13,7 @@ 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;
|
||||
|
||||
@ -51,6 +52,7 @@ public class OmsApplicationModule : FakeModule
|
||||
context.Services.AddTransient<ILabelPdfRenderer, QuestPdfLabelRenderer>();
|
||||
context.Services.AddTransient<LabelPdfGenerationService>();
|
||||
|
||||
context.Services.AddOmsFileStorage(configuration);
|
||||
context.Services.AddOmsSmallBagTrack(configuration);
|
||||
context.Services.AddOmsItfxIntegration(configuration);
|
||||
context.Services.AddOmsPlatformShopIntegration(configuration);
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
@ -15,7 +14,7 @@ namespace Oms.Application.Services.Orders;
|
||||
/// </summary>
|
||||
public sealed class OrderFileManager(
|
||||
IOrderFileRepository orderFileRepository,
|
||||
IWebHostEnvironment webHostEnvironment) : IScopedDependency
|
||||
IFileStorage fileStorage) : IScopedDependency
|
||||
{
|
||||
private static readonly HashSet<string> ImageExt =
|
||||
new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp" };
|
||||
@ -303,21 +302,10 @@ public sealed class OrderFileManager(
|
||||
}
|
||||
}
|
||||
|
||||
var webRoot = string.IsNullOrWhiteSpace(webHostEnvironment.WebRootPath)
|
||||
? Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot")
|
||||
: webHostEnvironment.WebRootPath;
|
||||
var relativeDir = Path.Combine("uploads", "order-files", orderId.ToString(), fileType);
|
||||
var absoluteDir = Path.Combine(webRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
var storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var absolutePath = Path.Combine(absoluteDir, storedName);
|
||||
await using (var stream = File.Create(absolutePath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
var url = $"/uploads/order-files/{orderId}/{fileType}/{storedName}".Replace('\\', '/');
|
||||
var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}";
|
||||
await using var stream = file.OpenReadStream();
|
||||
var url = await fileStorage.SaveAsync(stream, relativeKey, file.ContentType);
|
||||
return (url, Path.GetFileName(file.FileName), file.ContentType, file.Length);
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Messaging;
|
||||
using Oms.Application.Services.Orders.Templates;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.OrdersAggregate;
|
||||
using Oms.Domain.ChannelsAggregate.Consts;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
@ -487,13 +488,15 @@ public partial class OrderService
|
||||
throw new BusinessException("自定义模板样例文件不存在,请重新上传");
|
||||
}
|
||||
|
||||
var absolutePath = OmsFileUploadHelper.ResolveWebRootAbsolutePath(WebHostEnvironment, template.SourceFilePath);
|
||||
if (!File.Exists(absolutePath))
|
||||
if (!await FileStorage.ExistsAsync(template.SourceFilePath))
|
||||
{
|
||||
throw new BusinessException("自定义模板样例文件不存在,请重新上传");
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(absolutePath);
|
||||
await using var stream = await FileStorage.OpenReadAsync(template.SourceFilePath);
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
var bytes = ms.ToArray();
|
||||
var downloadName = string.IsNullOrWhiteSpace(template.SourceFileName)
|
||||
? $"{template.Name}.xlsx"
|
||||
: template.SourceFileName;
|
||||
@ -609,8 +612,6 @@ public partial class OrderService
|
||||
|
||||
private async Task<string> CreateOrderPrintFileAsync(OrderOutput order)
|
||||
{
|
||||
var directory = Path.Combine(GetWebRootDirectory(), "uploads", "prints", "orders");
|
||||
Directory.CreateDirectory(directory);
|
||||
var fileName = $"{order.ServiceNo}-{DateTime.Now:yyyyMMddHHmmss}.html";
|
||||
var html = $"""
|
||||
<!DOCTYPE html>
|
||||
@ -625,8 +626,9 @@ public partial class OrderService
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
await File.WriteAllTextAsync(Path.Combine(directory, fileName), html, Encoding.UTF8);
|
||||
return $"/uploads/prints/orders/{fileName}";
|
||||
var relativeKey = $"uploads/prints/orders/{fileName}";
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(html));
|
||||
return await FileStorage.SaveAsync(stream, relativeKey, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
private string GetOrderExportDirectory() => Path.Combine(GetWebRootDirectory(), "uploads", "exports", "orders");
|
||||
@ -640,6 +642,17 @@ public partial class OrderService
|
||||
return webRoot;
|
||||
}
|
||||
|
||||
private async Task CleanupExpiredLocalExportFilesAsync()
|
||||
{
|
||||
// COS 侧建议用生命周期规则清理;本地仍清理 24h 前导出文件。
|
||||
if (FileStorage is not LocalFileStorage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
|
||||
}
|
||||
|
||||
private async Task CleanupExpiredFilesAsync(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
|
||||
@ -77,7 +77,7 @@ public partial class OrderService
|
||||
var (relativePath, originalName) = await OmsFileUploadHelper.SaveOrderTemplateSampleAsync(
|
||||
input.File,
|
||||
customerId,
|
||||
WebHostEnvironment);
|
||||
FileStorage);
|
||||
|
||||
var suggested = OrderTemplateHeaderMatcher.BuildSuggestedMapping(OrderTemplateKeys.DedicatedSmallParcelStandard, headers);
|
||||
var entity = new OrderTemplate
|
||||
|
||||
@ -8,6 +8,7 @@ using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.DataPerm;
|
||||
using Oms.Application.Labeling;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Newtonsoft.Json;
|
||||
using Oms.Application.Services.Orders.Batch;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
@ -34,6 +35,7 @@ public partial class OrderService : ApplicationService
|
||||
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>();
|
||||
private IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService<IPickupOrderRepository>();
|
||||
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
|
||||
private LabelPdfGenerationService LabelPdfGenerationService =>
|
||||
LazyServiceProvider.GetRequiredService<LabelPdfGenerationService>();
|
||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||
@ -323,7 +325,7 @@ public partial class OrderService : ApplicationService
|
||||
[HttpPost("/order/export")]
|
||||
public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input)
|
||||
{
|
||||
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
|
||||
await CleanupExpiredLocalExportFilesAsync();
|
||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
|
||||
CustomerRepository,
|
||||
CurrentUser,
|
||||
@ -339,12 +341,17 @@ public partial class OrderService : ApplicationService
|
||||
}
|
||||
|
||||
var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx";
|
||||
var directory = GetOrderExportDirectory();
|
||||
Directory.CreateDirectory(directory);
|
||||
await MiniExcel.SaveAsAsync(Path.Combine(directory, fileName), await BuildOrderExportRowsAsync(orders));
|
||||
var relativeKey = $"uploads/exports/orders/{fileName}";
|
||||
await using var stream = new MemoryStream();
|
||||
await MiniExcel.SaveAsAsync(stream, await BuildOrderExportRowsAsync(orders));
|
||||
stream.Position = 0;
|
||||
var downloadUrl = await FileStorage.SaveAsync(
|
||||
stream,
|
||||
relativeKey,
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
return new OrderExportOutput
|
||||
{
|
||||
DownloadUrl = $"/uploads/exports/orders/{fileName}",
|
||||
DownloadUrl = downloadUrl,
|
||||
ExpireAt = DateTime.Now.AddHours(24)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
using System.Linq.Expressions;
|
||||
using Fake.Application;
|
||||
using Fake.Domain.Exceptions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiniExcelLibs;
|
||||
using Oms.Application.Dtos.Recharge;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.RechargeAggregate.Entities;
|
||||
using Oms.Domain.RechargeAggregate.Enums;
|
||||
@ -22,7 +22,7 @@ public class RechargeService : ApplicationService
|
||||
{
|
||||
private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService<IRechargeRecordRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
|
||||
|
||||
/// <summary>
|
||||
/// 提交充值申请。
|
||||
@ -45,7 +45,7 @@ public class RechargeService : ApplicationService
|
||||
|
||||
var screenshotPath = await OmsFileUploadHelper.SaveRechargeScreenshotAsync(
|
||||
input.RechargeScreenshot,
|
||||
WebHostEnvironment);
|
||||
FileStorage);
|
||||
|
||||
var entity = new RechargeRecord
|
||||
{
|
||||
|
||||
@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Warehouse;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.WarehouseAggregate.Consts;
|
||||
using Oms.Domain.WarehouseAggregate.Entities;
|
||||
|
||||
@ -211,7 +212,7 @@ public partial class WarehouseService
|
||||
public async Task<object> UploadParcelImageAsync(IFormFile file)
|
||||
{
|
||||
await ResolveMutationCustomerIdAsync(null);
|
||||
var path = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, WebHostEnvironment);
|
||||
var path = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, FileStorage);
|
||||
return new { url = path, fileUrl = path };
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
using System.Linq.Expressions;
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Warehouse;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
|
||||
namespace Oms.Application.Services.Warehouse;
|
||||
|
||||
public partial class WarehouseService
|
||||
{
|
||||
private IWebHostEnvironment WebHostEnvironment =>
|
||||
LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
private IFileStorage FileStorage =>
|
||||
LazyServiceProvider.GetRequiredService<IFileStorage>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询 SKU 列表。
|
||||
@ -166,7 +166,7 @@ public partial class WarehouseService
|
||||
public async Task<object> UploadSkuImageAsync(IFormFile file)
|
||||
{
|
||||
await ResolveMutationCustomerIdAsync(null);
|
||||
var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, WebHostEnvironment);
|
||||
var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, FileStorage);
|
||||
return new { url = path, fileUrl = path };
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Xhs;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.XhsAggregate.Entities;
|
||||
using Oms.Domain.XhsAggregate.Repositories;
|
||||
@ -15,7 +15,7 @@ public class XhsMediaService : ApplicationService
|
||||
{
|
||||
private IXhsMediaRepository MediaRepository => LazyServiceProvider.GetRequiredService<IXhsMediaRepository>();
|
||||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
|
||||
|
||||
/// <summary>
|
||||
/// 上传媒体文件
|
||||
@ -29,25 +29,18 @@ public class XhsMediaService : ApplicationService
|
||||
}
|
||||
|
||||
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser);
|
||||
var uploadsRoot = Path.Combine(WebHostEnvironment.WebRootPath ?? "wwwroot", "uploads", "xhs", customerId.ToString());
|
||||
Directory.CreateDirectory(uploadsRoot);
|
||||
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
var storedName = $"{Guid.NewGuid():N}{ext}";
|
||||
var fullPath = Path.Combine(uploadsRoot, storedName);
|
||||
await using (var stream = File.Create(fullPath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
var relativeKey = $"uploads/xhs/{customerId}/{storedName}";
|
||||
|
||||
var relativePath = Path.Combine("uploads", "xhs", customerId.ToString(), storedName).Replace('\\', '/');
|
||||
var publicUrl = $"/{relativePath}";
|
||||
await using var stream = file.OpenReadStream();
|
||||
var publicUrl = await FileStorage.SaveAsync(stream, relativeKey, file.ContentType);
|
||||
|
||||
var media = new XhsMedia
|
||||
{
|
||||
CustomerId = customerId,
|
||||
FileName = file.FileName,
|
||||
StoragePath = relativePath,
|
||||
StoragePath = relativeKey,
|
||||
PublicUrl = publicUrl,
|
||||
MimeType = file.ContentType,
|
||||
FileSizeBytes = file.Length
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Oms.Application.Storage;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// OMS 上传文件落盘(wwwroot/uploads)。
|
||||
/// OMS 上传文件辅助(经 IFileStorage,支持本地 / 腾讯云 COS)。
|
||||
/// </summary>
|
||||
public static class OmsFileUploadHelper
|
||||
{
|
||||
@ -15,26 +15,26 @@ public static class OmsFileUploadHelper
|
||||
private const long MaxImageBytes = 5 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// 保存充值截图,返回以 /uploads 开头的相对访问路径。
|
||||
/// 保存充值截图,返回可访问 URL。
|
||||
/// </summary>
|
||||
public static Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) =>
|
||||
SaveImageAsync(file, env, "recharge", "充值截图");
|
||||
public static Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IFileStorage storage) =>
|
||||
SaveImageAsync(file, storage, "recharge", "充值截图");
|
||||
|
||||
/// <summary>
|
||||
/// 保存仓储 SKU 图片。
|
||||
/// </summary>
|
||||
public static Task<string> SaveWarehouseSkuImageAsync(IFormFile file, IWebHostEnvironment env) =>
|
||||
SaveImageAsync(file, env, "sku", "SKU 图片");
|
||||
public static Task<string> SaveWarehouseSkuImageAsync(IFormFile file, IFileStorage storage) =>
|
||||
SaveImageAsync(file, storage, "sku", "SKU 图片");
|
||||
|
||||
/// <summary>
|
||||
/// 保存包裹商品图片。
|
||||
/// </summary>
|
||||
public static Task<string> SaveWarehouseParcelImageAsync(IFormFile file, IWebHostEnvironment env) =>
|
||||
SaveImageAsync(file, env, "parcel", "包裹商品图片");
|
||||
public static Task<string> SaveWarehouseParcelImageAsync(IFormFile file, IFileStorage storage) =>
|
||||
SaveImageAsync(file, storage, "parcel", "包裹商品图片");
|
||||
|
||||
private static async Task<string> SaveImageAsync(
|
||||
IFormFile? file,
|
||||
IWebHostEnvironment env,
|
||||
IFileStorage storage,
|
||||
string folder,
|
||||
string label)
|
||||
{
|
||||
@ -64,25 +64,12 @@ public static class OmsFileUploadHelper
|
||||
throw new BusinessException($"{label}仅支持 jpg、png、webp、gif、bmp 格式");
|
||||
}
|
||||
|
||||
var webRoot = env.WebRootPath;
|
||||
if (string.IsNullOrWhiteSpace(webRoot))
|
||||
{
|
||||
webRoot = Path.Combine(env.ContentRootPath, "wwwroot");
|
||||
}
|
||||
|
||||
var monthFolder = DateTime.Now.ToString("yyyyMM");
|
||||
var relativeDir = Path.Combine("uploads", folder, monthFolder);
|
||||
var absoluteDir = Path.Combine(webRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
var fileName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var absolutePath = Path.Combine(absoluteDir, fileName);
|
||||
await using (var stream = File.Create(absolutePath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
var relativeKey = $"uploads/{folder}/{monthFolder}/{fileName}";
|
||||
|
||||
return $"/uploads/{folder}/{monthFolder}/{fileName}".Replace('\\', '/');
|
||||
await using var stream = file.OpenReadStream();
|
||||
return await storage.SaveAsync(stream, relativeKey, file.ContentType);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> ExcelExtensions =
|
||||
@ -96,7 +83,7 @@ public static class OmsFileUploadHelper
|
||||
public static async Task<(string RelativePath, string FileName)> SaveOrderTemplateSampleAsync(
|
||||
IFormFile file,
|
||||
int customerId,
|
||||
IWebHostEnvironment env)
|
||||
IFileStorage storage)
|
||||
{
|
||||
if (file == null || file.Length <= 0)
|
||||
{
|
||||
@ -114,37 +101,15 @@ public static class OmsFileUploadHelper
|
||||
throw new BusinessException("仅支持 .xlsx / .xls 文件");
|
||||
}
|
||||
|
||||
var webRoot = env.WebRootPath;
|
||||
if (string.IsNullOrWhiteSpace(webRoot))
|
||||
{
|
||||
webRoot = Path.Combine(env.ContentRootPath, "wwwroot");
|
||||
}
|
||||
|
||||
var relativeDir = Path.Combine("uploads", "order-templates", customerId.ToString());
|
||||
var absoluteDir = Path.Combine(webRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
var fileName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var absolutePath = Path.Combine(absoluteDir, fileName);
|
||||
await using (var stream = File.Create(absolutePath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
var relativeKey = $"uploads/order-templates/{customerId}/{fileName}";
|
||||
|
||||
var relativePath = $"/uploads/order-templates/{customerId}/{fileName}".Replace('\\', '/');
|
||||
return (relativePath, Path.GetFileName(file.FileName));
|
||||
}
|
||||
|
||||
public static string ResolveWebRootAbsolutePath(IWebHostEnvironment env, string relativePath)
|
||||
{
|
||||
var webRoot = env.WebRootPath;
|
||||
if (string.IsNullOrWhiteSpace(webRoot))
|
||||
{
|
||||
webRoot = Path.Combine(env.ContentRootPath, "wwwroot");
|
||||
}
|
||||
|
||||
var trimmed = relativePath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(webRoot, trimmed);
|
||||
await using var stream = file.OpenReadStream();
|
||||
var url = await storage.SaveAsync(
|
||||
stream,
|
||||
relativeKey,
|
||||
file.ContentType ?? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
return (url, Path.GetFileName(file.FileName));
|
||||
}
|
||||
|
||||
private static string? GuessExtensionFromContentType(string? contentType) => contentType?.ToLowerInvariant() switch
|
||||
|
||||
54
src/Oms.Application/Storage/FileStorageOptions.cs
Normal file
54
src/Oms.Application/Storage/FileStorageOptions.cs
Normal file
@ -0,0 +1,54 @@
|
||||
namespace Oms.Application.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 文件存储配置。Provider=Local 落盘 wwwroot;Provider=Cos 走腾讯云 COS。
|
||||
/// </summary>
|
||||
public class FileStorageOptions
|
||||
{
|
||||
public const string SectionName = "Oms:FileStorage";
|
||||
|
||||
/// <summary>
|
||||
/// 存储提供方:Local | Cos。
|
||||
/// </summary>
|
||||
public string Provider { get; set; } = "Local";
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云 COS 配置(Provider=Cos 时必填)。
|
||||
/// </summary>
|
||||
public TencentCosOptions Cos { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云 COS 连接参数。
|
||||
/// </summary>
|
||||
public class TencentCosOptions
|
||||
{
|
||||
/// <summary>API 密钥 SecretId。</summary>
|
||||
public string SecretId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>API 密钥 SecretKey。</summary>
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>地域,如 ap-guangzhou。</summary>
|
||||
public string Region { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>存储桶,格式 bucketname-APPID。</summary>
|
||||
public string Bucket { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 对外访问根地址。可填 CDN / 自定义域名,末尾不要斜杠。
|
||||
/// 空则默认 https://{Bucket}.cos.{Region}.myqcloud.com
|
||||
/// </summary>
|
||||
public string? BaseUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对象键前缀(如 oms/prod/),可空。
|
||||
/// </summary>
|
||||
public string KeyPrefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>签名有效期(秒)。</summary>
|
||||
public long DurationSecond { get; set; } = 600;
|
||||
|
||||
/// <summary>是否 HTTPS。</summary>
|
||||
public bool IsHttps { get; set; } = true;
|
||||
}
|
||||
26
src/Oms.Application/Storage/FileStorageServiceExtensions.cs
Normal file
26
src/Oms.Application/Storage/FileStorageServiceExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Oms.Application.Storage;
|
||||
|
||||
public static class FileStorageServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddOmsFileStorage(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<FileStorageOptions>(configuration.GetSection(FileStorageOptions.SectionName));
|
||||
|
||||
var provider = configuration.GetSection(FileStorageOptions.SectionName)["Provider"]
|
||||
?? "Local";
|
||||
|
||||
if (string.Equals(provider, "Cos", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
services.AddSingleton<IFileStorage, TencentCosFileStorage>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IFileStorage, LocalFileStorage>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
36
src/Oms.Application/Storage/IFileStorage.cs
Normal file
36
src/Oms.Application/Storage/IFileStorage.cs
Normal file
@ -0,0 +1,36 @@
|
||||
namespace Oms.Application.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 统一文件存储(本地 / 腾讯云 COS)。
|
||||
/// </summary>
|
||||
public interface IFileStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// 保存文件。
|
||||
/// </summary>
|
||||
/// <param name="content">文件流。</param>
|
||||
/// <param name="relativeKey">相对键,如 uploads/avatars/a.jpg(不要前导 /)。</param>
|
||||
/// <param name="contentType">MIME,可选。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>可对外访问的 URL(本地为 /uploads/...,COS 为完整 URL)。</returns>
|
||||
Task<string> SaveAsync(
|
||||
Stream content,
|
||||
string relativeKey,
|
||||
string? contentType = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 打开已保存文件的只读流。pathOrUrl 可为相对路径或完整 URL。
|
||||
/// </summary>
|
||||
Task<Stream> OpenReadAsync(string pathOrUrl, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 判断文件是否存在。
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(string pathOrUrl, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件(不存在时忽略)。
|
||||
/// </summary>
|
||||
Task DeleteAsync(string pathOrUrl, CancellationToken cancellationToken = default);
|
||||
}
|
||||
116
src/Oms.Application/Storage/LocalFileStorage.cs
Normal file
116
src/Oms.Application/Storage/LocalFileStorage.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace Oms.Application.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 本地 wwwroot 文件存储。
|
||||
/// </summary>
|
||||
public sealed class LocalFileStorage(IWebHostEnvironment environment) : IFileStorage
|
||||
{
|
||||
public async Task<string> SaveAsync(
|
||||
Stream content,
|
||||
string relativeKey,
|
||||
string? contentType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = NormalizeRelativeKey(relativeKey);
|
||||
var absolutePath = ToAbsolutePath(key);
|
||||
var directory = Path.GetDirectoryName(absolutePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
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.Replace('\\', '/');
|
||||
}
|
||||
|
||||
public Task<Stream> 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<bool> 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;
|
||||
}
|
||||
|
||||
var key = NormalizeRelativeKey(pathOrUrl);
|
||||
return ToAbsolutePath(key);
|
||||
}
|
||||
|
||||
private string ToAbsolutePath(string relativeKey)
|
||||
{
|
||||
var webRoot = string.IsNullOrWhiteSpace(environment.WebRootPath)
|
||||
? Path.Combine(environment.ContentRootPath, "wwwroot")
|
||||
: environment.WebRootPath;
|
||||
return Path.Combine(webRoot, relativeKey.Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
private static string NormalizeRelativeKey(string relativeKey)
|
||||
{
|
||||
var key = relativeKey.Trim().Replace('\\', '/').TrimStart('/');
|
||||
if (key.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var uri = new Uri(key);
|
||||
key = uri.AbsolutePath.TrimStart('/');
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ArgumentException("relativeKey 不能为空", nameof(relativeKey));
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
217
src/Oms.Application/Storage/TencentCosFileStorage.cs
Normal file
217
src/Oms.Application/Storage/TencentCosFileStorage.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using COSXML;
|
||||
using COSXML.Auth;
|
||||
using COSXML.Model.Object;
|
||||
using Fake.Application;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oms.Application.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯云 COS 文件存储。
|
||||
/// </summary>
|
||||
public sealed class TencentCosFileStorage : IFileStorage, IDisposable
|
||||
{
|
||||
private readonly TencentCosOptions _options;
|
||||
private readonly CosXmlServer _cosXml;
|
||||
private readonly ILogger<TencentCosFileStorage> _logger;
|
||||
private readonly string _publicBaseUrl;
|
||||
|
||||
public TencentCosFileStorage(
|
||||
IOptions<FileStorageOptions> options,
|
||||
ILogger<TencentCosFileStorage> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_options = options.Value.Cos ?? new TencentCosOptions();
|
||||
ValidateOptions(_options);
|
||||
|
||||
var config = new CosXmlConfig.Builder()
|
||||
.IsHttps(_options.IsHttps)
|
||||
.SetRegion(_options.Region)
|
||||
.SetAppid(ExtractAppId(_options.Bucket))
|
||||
.Build();
|
||||
|
||||
var credential = new DefaultQCloudCredentialProvider(
|
||||
_options.SecretId,
|
||||
_options.SecretKey,
|
||||
_options.DurationSecond);
|
||||
|
||||
_cosXml = new CosXmlServer(config, credential);
|
||||
_publicBaseUrl = ResolvePublicBaseUrl(_options);
|
||||
}
|
||||
|
||||
public Task<string> SaveAsync(
|
||||
Stream content,
|
||||
string relativeKey,
|
||||
string? contentType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var objectKey = ToObjectKey(relativeKey);
|
||||
if (content.CanSeek)
|
||||
{
|
||||
content.Position = 0;
|
||||
}
|
||||
|
||||
// SDK PutObject(Stream) 需要可定位流;不可定位时先拷到内存。
|
||||
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}");
|
||||
}
|
||||
|
||||
var url = $"{_publicBaseUrl}/{objectKey}";
|
||||
_logger.LogDebug("COS 上传成功: {ObjectKey}", objectKey);
|
||||
return Task.FromResult(url);
|
||||
}
|
||||
finally
|
||||
{
|
||||
owned?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Stream> 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<bool> 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()
|
||||
{
|
||||
// CosXmlServer 无显式 Dispose;保留以便未来扩展。
|
||||
}
|
||||
|
||||
private string ToObjectKey(string pathOrUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pathOrUrl))
|
||||
{
|
||||
throw new ArgumentException("路径不能为空", nameof(pathOrUrl));
|
||||
}
|
||||
|
||||
var key = pathOrUrl.Trim().Replace('\\', '/');
|
||||
if (key.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var uri = new Uri(key);
|
||||
key = uri.AbsolutePath.TrimStart('/');
|
||||
}
|
||||
else
|
||||
{
|
||||
key = key.TrimStart('/');
|
||||
}
|
||||
|
||||
var prefix = (_options.KeyPrefix ?? string.Empty).Replace('\\', '/').Trim('/');
|
||||
if (!string.IsNullOrEmpty(prefix))
|
||||
{
|
||||
// 若已带前缀则不重复追加(兼容完整 URL 回读)。
|
||||
if (!key.StartsWith(prefix + "/", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(key, prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
key = $"{prefix}/{key}";
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new ArgumentException("对象键不能为空", nameof(pathOrUrl));
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Enums;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
@ -36,13 +37,13 @@ public partial class AuthService(
|
||||
AccountManager accountManager,
|
||||
IJwtService jwtService,
|
||||
IUserRepository userRepository,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
ICustomerRepository customerRepository,
|
||||
IWeChatLoginSessionStore weChatLoginSessionStore,
|
||||
IMemoryCache memoryCache,
|
||||
IRbacAuthCache rbacAuthCache)
|
||||
IRbacAuthCache rbacAuthCache,
|
||||
IFileStorage fileStorage)
|
||||
: ApplicationService
|
||||
{
|
||||
private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3);
|
||||
@ -384,12 +385,10 @@ public partial class AuthService(
|
||||
throw new DomainException("文件大小不能超过10MB");
|
||||
}
|
||||
|
||||
var uploadPath = Path.Combine(webHostEnvironment.WebRootPath ?? webHostEnvironment.ContentRootPath, "uploads", "avatars");
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
|
||||
var fileName = $"{userId}_{DateTime.Now:yyyyMMddHHmmss}{extension}";
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
var fileName = $"{userId}_{DateTime.Now:yyyyMMddHHmmss}.jpg";
|
||||
var relativeKey = $"uploads/avatars/{fileName}";
|
||||
|
||||
await using var output = new MemoryStream();
|
||||
using (var stream = file.OpenReadStream())
|
||||
using (var image = await Image.LoadAsync(stream, cancellationToken))
|
||||
{
|
||||
@ -412,13 +411,14 @@ public partial class AuthService(
|
||||
}
|
||||
|
||||
image.Mutate(x => x.Resize(width, height));
|
||||
await image.SaveAsJpegAsync(filePath, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
|
||||
await image.SaveAsJpegAsync(output, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
|
||||
{
|
||||
Quality = 80
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
var avatarUrl = $"/uploads/avatars/{fileName}";
|
||||
output.Position = 0;
|
||||
var avatarUrl = await fileStorage.SaveAsync(output, relativeKey, "image/jpeg", cancellationToken);
|
||||
var user = await userRepository.FirstAsync(u => u.Id == userId, cancellationToken: cancellationToken);
|
||||
user.UpdateAvatar(avatarUrl);
|
||||
await userRepository.UpdateAsync(user, cancellationToken: cancellationToken);
|
||||
|
||||
@ -60,6 +60,19 @@
|
||||
}
|
||||
},
|
||||
"Oms": {
|
||||
"FileStorage": {
|
||||
"Provider": "Local",
|
||||
"Cos": {
|
||||
"SecretId": "",
|
||||
"SecretKey": "",
|
||||
"Region": "ap-guangzhou",
|
||||
"Bucket": "your-bucket-1250000000",
|
||||
"BaseUrl": "",
|
||||
"KeyPrefix": "",
|
||||
"DurationSecond": 600,
|
||||
"IsHttps": true
|
||||
}
|
||||
},
|
||||
"TrackService": {
|
||||
"Enabled": true,
|
||||
"BaseUrl": "http://api.nanafa.com/track",
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"UseLocalConfigs": false,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"App": {
|
||||
"CorsOrigins": [
|
||||
"https://ideship.com",
|
||||
"https://www.ideship.com"
|
||||
],
|
||||
"SendStartupNotification": false
|
||||
},
|
||||
"WeChat": {
|
||||
"Mp": {
|
||||
"OAuthRedirectBase": "https://ideship.com",
|
||||
"FollowUrl": ""
|
||||
}
|
||||
},
|
||||
"Oms": {
|
||||
"TrackService": {
|
||||
"Enabled": true,
|
||||
"BaseUrl": "https://api.ideship.com/track"
|
||||
},
|
||||
"Itfx": {
|
||||
"BaseUrl": "https://api.ideship.com/itfx"
|
||||
},
|
||||
"PlatformShop": {
|
||||
"FrontendBaseUrl": "https://ideship.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user