Compare commits

...

5 Commits

Author SHA1 Message Date
943ba539a4 feat: cos
All checks were successful
Build and Deploy / deploy (push) Successful in 1m43s
2026-09-10 17:32:49 +08:00
23eb80931a Merge remote-tracking branch 'prod/master' 2026-09-09 15:52:35 +08:00
c886e8cef3 Merge remote-tracking branch 'prod/master' 2026-09-09 14:36:10 +08:00
8f7a4c1405 Merge remote-tracking branch 'origin/master' 2026-09-09 14:35:46 +08:00
93bef462fb feat: 迁移到gitea 2026-09-05 16:33:28 +08:00
20 changed files with 554 additions and 161 deletions

View File

@ -1,8 +1,8 @@
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Services.Orders; using Oms.Application.Services.Orders;
using Oms.Application.Storage;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
@ -22,7 +22,7 @@ public sealed class LabelPdfGenerationService(
ISystemOtherConfigRepository systemOtherConfigRepository, ISystemOtherConfigRepository systemOtherConfigRepository,
IOrderRepository orderRepository, IOrderRepository orderRepository,
OrderFileManager orderFileManager, OrderFileManager orderFileManager,
IWebHostEnvironment webHostEnvironment, IFileStorage fileStorage,
ILogger<LabelPdfGenerationService> logger) ILogger<LabelPdfGenerationService> logger)
{ {
public async Task<LabelPdfGenerateResult> GenerateAndPersistAsync( public async Task<LabelPdfGenerateResult> GenerateAndPersistAsync(
@ -158,11 +158,6 @@ public sealed class LabelPdfGenerationService(
private async Task<string> SavePdfAsync(string serviceNo, byte[] pdfBytes, CancellationToken cancellationToken) 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(); var safeNo = string.IsNullOrWhiteSpace(serviceNo) ? "order" : serviceNo.Trim();
foreach (var c in Path.GetInvalidFileNameChars()) foreach (var c in Path.GetInvalidFileNameChars())
{ {
@ -170,9 +165,9 @@ public sealed class LabelPdfGenerationService(
} }
var fileName = $"{safeNo}-{DateTime.Now:yyyyMMddHHmmss}.pdf"; var fileName = $"{safeNo}-{DateTime.Now:yyyyMMddHHmmss}.pdf";
var absolute = Path.Combine(directory, fileName); var relativeKey = $"uploads/prints/orders/{fileName}";
await File.WriteAllBytesAsync(absolute, pdfBytes, cancellationToken); await using var stream = new MemoryStream(pdfBytes);
return $"/uploads/prints/orders/{fileName}"; return await fileStorage.SaveAsync(stream, relativeKey, "application/pdf", cancellationToken);
} }
} }

View File

@ -29,6 +29,7 @@
<PackageReference Include="MiniExcel" Version="1.43.1" /> <PackageReference Include="MiniExcel" Version="1.43.1" />
<PackageReference Include="QuestPDF" Version="2025.7.0" /> <PackageReference Include="QuestPDF" Version="2025.7.0" />
<PackageReference Include="Serilog" Version="4.2.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" Version="0.16.10" />
<PackageReference Include="ZXing.Net.Bindings.ImageSharp.V3" Version="0.16.15" /> <PackageReference Include="ZXing.Net.Bindings.ImageSharp.V3" Version="0.16.15" />
</ItemGroup> </ItemGroup>

View File

@ -13,6 +13,7 @@ using Oms.Application.Integrations.PlatformShop;
using Oms.Application.Labeling; using Oms.Application.Labeling;
using Oms.Application.Labeling.Hiprint; using Oms.Application.Labeling.Hiprint;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Application.Workers; using Oms.Application.Workers;
using Oms.Domain; using Oms.Domain;
@ -51,6 +52,7 @@ public class OmsApplicationModule : FakeModule
context.Services.AddTransient<ILabelPdfRenderer, QuestPdfLabelRenderer>(); context.Services.AddTransient<ILabelPdfRenderer, QuestPdfLabelRenderer>();
context.Services.AddTransient<LabelPdfGenerationService>(); context.Services.AddTransient<LabelPdfGenerationService>();
context.Services.AddOmsFileStorage(configuration);
context.Services.AddOmsSmallBagTrack(configuration); context.Services.AddOmsSmallBagTrack(configuration);
context.Services.AddOmsItfxIntegration(configuration); context.Services.AddOmsItfxIntegration(configuration);
context.Services.AddOmsPlatformShopIntegration(configuration); context.Services.AddOmsPlatformShopIntegration(configuration);

View File

@ -1,9 +1,8 @@
using Fake.Application; using Fake.Application;
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Shared; using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Repositories;
@ -15,7 +14,7 @@ namespace Oms.Application.Services.Orders;
/// </summary> /// </summary>
public sealed class OrderFileManager( public sealed class OrderFileManager(
IOrderFileRepository orderFileRepository, IOrderFileRepository orderFileRepository,
IWebHostEnvironment webHostEnvironment) : IScopedDependency IFileStorage fileStorage) : IScopedDependency
{ {
private static readonly HashSet<string> ImageExt = private static readonly HashSet<string> ImageExt =
new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp" }; 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 storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
var absolutePath = Path.Combine(absoluteDir, storedName); var relativeKey = $"uploads/order-files/{orderId}/{fileType}/{storedName}";
await using (var stream = File.Create(absolutePath)) await using var stream = file.OpenReadStream();
{ var url = await fileStorage.SaveAsync(stream, relativeKey, file.ContentType);
await file.CopyToAsync(stream);
}
var url = $"/uploads/order-files/{orderId}/{fileType}/{storedName}".Replace('\\', '/');
return (url, Path.GetFileName(file.FileName), file.ContentType, file.Length); return (url, Path.GetFileName(file.FileName), file.ContentType, file.Length);
} }

View File

@ -12,6 +12,7 @@ using Oms.Application.Dtos.Shared;
using Oms.Application.Messaging; using Oms.Application.Messaging;
using Oms.Application.Services.Orders.Templates; using Oms.Application.Services.Orders.Templates;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate; using Oms.Domain.OrdersAggregate;
using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Consts;
using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Entities;
@ -487,13 +488,15 @@ public partial class OrderService
throw new BusinessException("自定义模板样例文件不存在,请重新上传"); throw new BusinessException("自定义模板样例文件不存在,请重新上传");
} }
var absolutePath = OmsFileUploadHelper.ResolveWebRootAbsolutePath(WebHostEnvironment, template.SourceFilePath); if (!await FileStorage.ExistsAsync(template.SourceFilePath))
if (!File.Exists(absolutePath))
{ {
throw new BusinessException("自定义模板样例文件不存在,请重新上传"); 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) var downloadName = string.IsNullOrWhiteSpace(template.SourceFileName)
? $"{template.Name}.xlsx" ? $"{template.Name}.xlsx"
: template.SourceFileName; : template.SourceFileName;
@ -609,8 +612,6 @@ public partial class OrderService
private async Task<string> CreateOrderPrintFileAsync(OrderOutput order) 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 fileName = $"{order.ServiceNo}-{DateTime.Now:yyyyMMddHHmmss}.html";
var html = $""" var html = $"""
<!DOCTYPE html> <!DOCTYPE html>
@ -625,8 +626,9 @@ public partial class OrderService
</body> </body>
</html> </html>
"""; """;
await File.WriteAllTextAsync(Path.Combine(directory, fileName), html, Encoding.UTF8); var relativeKey = $"uploads/prints/orders/{fileName}";
return $"/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"); private string GetOrderExportDirectory() => Path.Combine(GetWebRootDirectory(), "uploads", "exports", "orders");
@ -640,6 +642,17 @@ public partial class OrderService
return webRoot; return webRoot;
} }
private async Task CleanupExpiredLocalExportFilesAsync()
{
// COS 侧建议用生命周期规则清理;本地仍清理 24h 前导出文件。
if (FileStorage is not LocalFileStorage)
{
return;
}
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
}
private async Task CleanupExpiredFilesAsync(string directory) private async Task CleanupExpiredFilesAsync(string directory)
{ {
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))

View File

@ -77,7 +77,7 @@ public partial class OrderService
var (relativePath, originalName) = await OmsFileUploadHelper.SaveOrderTemplateSampleAsync( var (relativePath, originalName) = await OmsFileUploadHelper.SaveOrderTemplateSampleAsync(
input.File, input.File,
customerId, customerId,
WebHostEnvironment); FileStorage);
var suggested = OrderTemplateHeaderMatcher.BuildSuggestedMapping(OrderTemplateKeys.DedicatedSmallParcelStandard, headers); var suggested = OrderTemplateHeaderMatcher.BuildSuggestedMapping(OrderTemplateKeys.DedicatedSmallParcelStandard, headers);
var entity = new OrderTemplate var entity = new OrderTemplate

View File

@ -8,6 +8,7 @@ using Oms.Application.Dtos.Shared;
using Oms.Application.DataPerm; using Oms.Application.DataPerm;
using Oms.Application.Labeling; using Oms.Application.Labeling;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Newtonsoft.Json; using Newtonsoft.Json;
using Oms.Application.Services.Orders.Batch; using Oms.Application.Services.Orders.Batch;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
@ -34,6 +35,7 @@ public partial class OrderService : ApplicationService
private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>(); private IDictRepository DictRepository => LazyServiceProvider.GetRequiredService<IDictRepository>();
private IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService<IPickupOrderRepository>(); private IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService<IPickupOrderRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>(); private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
private LabelPdfGenerationService LabelPdfGenerationService => private LabelPdfGenerationService LabelPdfGenerationService =>
LazyServiceProvider.GetRequiredService<LabelPdfGenerationService>(); LazyServiceProvider.GetRequiredService<LabelPdfGenerationService>();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver => private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
@ -323,7 +325,7 @@ public partial class OrderService : ApplicationService
[HttpPost("/order/export")] [HttpPost("/order/export")]
public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input) public async Task<OrderExportOutput> ExportAsync([FromBody] OrderExportInput input)
{ {
await CleanupExpiredFilesAsync(GetOrderExportDirectory()); await CleanupExpiredLocalExportFilesAsync();
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository, CustomerRepository,
CurrentUser, CurrentUser,
@ -339,12 +341,17 @@ public partial class OrderService : ApplicationService
} }
var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx"; var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx";
var directory = GetOrderExportDirectory(); var relativeKey = $"uploads/exports/orders/{fileName}";
Directory.CreateDirectory(directory); await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(Path.Combine(directory, fileName), await BuildOrderExportRowsAsync(orders)); 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 return new OrderExportOutput
{ {
DownloadUrl = $"/uploads/exports/orders/{fileName}", DownloadUrl = downloadUrl,
ExpireAt = DateTime.Now.AddHours(24) ExpireAt = DateTime.Now.AddHours(24)
}; };
} }

View File

@ -1,12 +1,12 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using Fake.Application; using Fake.Application;
using Fake.Domain.Exceptions; using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs; using MiniExcelLibs;
using Oms.Application.Dtos.Recharge; using Oms.Application.Dtos.Recharge;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.RechargeAggregate.Entities; using Oms.Domain.RechargeAggregate.Entities;
using Oms.Domain.RechargeAggregate.Enums; using Oms.Domain.RechargeAggregate.Enums;
@ -22,7 +22,7 @@ public class RechargeService : ApplicationService
{ {
private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService<IRechargeRecordRepository>(); private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService<IRechargeRecordRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>(); private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
/// <summary> /// <summary>
/// 提交充值申请。 /// 提交充值申请。
@ -45,7 +45,7 @@ public class RechargeService : ApplicationService
var screenshotPath = await OmsFileUploadHelper.SaveRechargeScreenshotAsync( var screenshotPath = await OmsFileUploadHelper.SaveRechargeScreenshotAsync(
input.RechargeScreenshot, input.RechargeScreenshot,
WebHostEnvironment); FileStorage);
var entity = new RechargeRecord var entity = new RechargeRecord
{ {

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse; using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.WarehouseAggregate.Consts; using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Entities; using Oms.Domain.WarehouseAggregate.Entities;
@ -211,7 +212,7 @@ public partial class WarehouseService
public async Task<object> UploadParcelImageAsync(IFormFile file) public async Task<object> UploadParcelImageAsync(IFormFile file)
{ {
await ResolveMutationCustomerIdAsync(null); await ResolveMutationCustomerIdAsync(null);
var path = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, WebHostEnvironment); var path = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, FileStorage);
return new { url = path, fileUrl = path }; return new { url = path, fileUrl = path };
} }

View File

@ -1,18 +1,18 @@
using System.Linq.Expressions; using System.Linq.Expressions;
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse; using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
namespace Oms.Application.Services.Warehouse; namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService public partial class WarehouseService
{ {
private IWebHostEnvironment WebHostEnvironment => private IFileStorage FileStorage =>
LazyServiceProvider.GetRequiredService<IWebHostEnvironment>(); LazyServiceProvider.GetRequiredService<IFileStorage>();
/// <summary> /// <summary>
/// 查询 SKU 列表。 /// 查询 SKU 列表。
@ -166,7 +166,7 @@ public partial class WarehouseService
public async Task<object> UploadSkuImageAsync(IFormFile file) public async Task<object> UploadSkuImageAsync(IFormFile file)
{ {
await ResolveMutationCustomerIdAsync(null); await ResolveMutationCustomerIdAsync(null);
var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, WebHostEnvironment); var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, FileStorage);
return new { url = path, fileUrl = path }; return new { url = path, fileUrl = path };
} }

View File

@ -1,9 +1,9 @@
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Xhs; using Oms.Application.Dtos.Xhs;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.XhsAggregate.Entities; using Oms.Domain.XhsAggregate.Entities;
using Oms.Domain.XhsAggregate.Repositories; using Oms.Domain.XhsAggregate.Repositories;
@ -15,7 +15,7 @@ public class XhsMediaService : ApplicationService
{ {
private IXhsMediaRepository MediaRepository => LazyServiceProvider.GetRequiredService<IXhsMediaRepository>(); private IXhsMediaRepository MediaRepository => LazyServiceProvider.GetRequiredService<IXhsMediaRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>(); private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService<IFileStorage>();
/// <summary> /// <summary>
/// 上传媒体文件 /// 上传媒体文件
@ -29,25 +29,18 @@ public class XhsMediaService : ApplicationService
} }
var customerId = await OmsSessionHelper.ResolveXhsCustomerIdAsync(CustomerRepository, CurrentUser); 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 ext = Path.GetExtension(file.FileName);
var storedName = $"{Guid.NewGuid():N}{ext}"; var storedName = $"{Guid.NewGuid():N}{ext}";
var fullPath = Path.Combine(uploadsRoot, storedName); var relativeKey = $"uploads/xhs/{customerId}/{storedName}";
await using (var stream = File.Create(fullPath))
{
await file.CopyToAsync(stream);
}
var relativePath = Path.Combine("uploads", "xhs", customerId.ToString(), storedName).Replace('\\', '/'); await using var stream = file.OpenReadStream();
var publicUrl = $"/{relativePath}"; var publicUrl = await FileStorage.SaveAsync(stream, relativeKey, file.ContentType);
var media = new XhsMedia var media = new XhsMedia
{ {
CustomerId = customerId, CustomerId = customerId,
FileName = file.FileName, FileName = file.FileName,
StoragePath = relativePath, StoragePath = relativeKey,
PublicUrl = publicUrl, PublicUrl = publicUrl,
MimeType = file.ContentType, MimeType = file.ContentType,
FileSizeBytes = file.Length FileSizeBytes = file.Length

View File

@ -1,11 +1,11 @@
using Fake.Application; using Fake.Application;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Oms.Application.Storage;
namespace Oms.Application.Shared; namespace Oms.Application.Shared;
/// <summary> /// <summary>
/// OMS 上传文件落盘(wwwroot/uploads)。 /// OMS 上传文件辅助(经 IFileStorage,支持本地 / 腾讯云 COS)。
/// </summary> /// </summary>
public static class OmsFileUploadHelper public static class OmsFileUploadHelper
{ {
@ -15,26 +15,26 @@ public static class OmsFileUploadHelper
private const long MaxImageBytes = 5 * 1024 * 1024; private const long MaxImageBytes = 5 * 1024 * 1024;
/// <summary> /// <summary>
/// 保存充值截图,返回以 /uploads 开头的相对访问路径。 /// 保存充值截图,返回可访问 URL。
/// </summary> /// </summary>
public static Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) => public static Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IFileStorage storage) =>
SaveImageAsync(file, env, "recharge", "充值截图"); SaveImageAsync(file, storage, "recharge", "充值截图");
/// <summary> /// <summary>
/// 保存仓储 SKU 图片。 /// 保存仓储 SKU 图片。
/// </summary> /// </summary>
public static Task<string> SaveWarehouseSkuImageAsync(IFormFile file, IWebHostEnvironment env) => public static Task<string> SaveWarehouseSkuImageAsync(IFormFile file, IFileStorage storage) =>
SaveImageAsync(file, env, "sku", "SKU 图片"); SaveImageAsync(file, storage, "sku", "SKU 图片");
/// <summary> /// <summary>
/// 保存包裹商品图片。 /// 保存包裹商品图片。
/// </summary> /// </summary>
public static Task<string> SaveWarehouseParcelImageAsync(IFormFile file, IWebHostEnvironment env) => public static Task<string> SaveWarehouseParcelImageAsync(IFormFile file, IFileStorage storage) =>
SaveImageAsync(file, env, "parcel", "包裹商品图片"); SaveImageAsync(file, storage, "parcel", "包裹商品图片");
private static async Task<string> SaveImageAsync( private static async Task<string> SaveImageAsync(
IFormFile? file, IFormFile? file,
IWebHostEnvironment env, IFileStorage storage,
string folder, string folder,
string label) string label)
{ {
@ -64,25 +64,12 @@ public static class OmsFileUploadHelper
throw new BusinessException($"{label}仅支持 jpg、png、webp、gif、bmp 格式"); 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 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 fileName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
var absolutePath = Path.Combine(absoluteDir, fileName); var relativeKey = $"uploads/{folder}/{monthFolder}/{fileName}";
await using (var stream = File.Create(absolutePath))
{
await file.CopyToAsync(stream);
}
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 = private static readonly HashSet<string> ExcelExtensions =
@ -96,7 +83,7 @@ public static class OmsFileUploadHelper
public static async Task<(string RelativePath, string FileName)> SaveOrderTemplateSampleAsync( public static async Task<(string RelativePath, string FileName)> SaveOrderTemplateSampleAsync(
IFormFile file, IFormFile file,
int customerId, int customerId,
IWebHostEnvironment env) IFileStorage storage)
{ {
if (file == null || file.Length <= 0) if (file == null || file.Length <= 0)
{ {
@ -114,37 +101,15 @@ public static class OmsFileUploadHelper
throw new BusinessException("仅支持 .xlsx / .xls 文件"); 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 fileName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
var absolutePath = Path.Combine(absoluteDir, fileName); var relativeKey = $"uploads/order-templates/{customerId}/{fileName}";
await using (var stream = File.Create(absolutePath))
{
await file.CopyToAsync(stream);
}
var relativePath = $"/uploads/order-templates/{customerId}/{fileName}".Replace('\\', '/'); await using var stream = file.OpenReadStream();
return (relativePath, Path.GetFileName(file.FileName)); var url = await storage.SaveAsync(
} stream,
relativeKey,
public static string ResolveWebRootAbsolutePath(IWebHostEnvironment env, string relativePath) file.ContentType ?? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
{ return (url, Path.GetFileName(file.FileName));
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);
} }
private static string? GuessExtensionFromContentType(string? contentType) => contentType?.ToLowerInvariant() switch private static string? GuessExtensionFromContentType(string? contentType) => contentType?.ToLowerInvariant() switch

View 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;
}

View 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;
}
}

View 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);
}

View 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;
}
}

View 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");
}
}
}

View File

@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.CustomersAggregate.Entities; using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.CustomersAggregate.Enums;
using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories;
@ -36,13 +37,13 @@ public partial class AuthService(
AccountManager accountManager, AccountManager accountManager,
IJwtService jwtService, IJwtService jwtService,
IUserRepository userRepository, IUserRepository userRepository,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
FakeRbacDbContext rbacDbContext, FakeRbacDbContext rbacDbContext,
ICustomerRepository customerRepository, ICustomerRepository customerRepository,
IWeChatLoginSessionStore weChatLoginSessionStore, IWeChatLoginSessionStore weChatLoginSessionStore,
IMemoryCache memoryCache, IMemoryCache memoryCache,
IRbacAuthCache rbacAuthCache) IRbacAuthCache rbacAuthCache,
IFileStorage fileStorage)
: ApplicationService : ApplicationService
{ {
private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3); private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3);
@ -384,12 +385,10 @@ public partial class AuthService(
throw new DomainException("文件大小不能超过10MB"); throw new DomainException("文件大小不能超过10MB");
} }
var uploadPath = Path.Combine(webHostEnvironment.WebRootPath ?? webHostEnvironment.ContentRootPath, "uploads", "avatars"); var fileName = $"{userId}_{DateTime.Now:yyyyMMddHHmmss}.jpg";
Directory.CreateDirectory(uploadPath); var relativeKey = $"uploads/avatars/{fileName}";
var fileName = $"{userId}_{DateTime.Now:yyyyMMddHHmmss}{extension}";
var filePath = Path.Combine(uploadPath, fileName);
await using var output = new MemoryStream();
using (var stream = file.OpenReadStream()) using (var stream = file.OpenReadStream())
using (var image = await Image.LoadAsync(stream, cancellationToken)) using (var image = await Image.LoadAsync(stream, cancellationToken))
{ {
@ -412,13 +411,14 @@ public partial class AuthService(
} }
image.Mutate(x => x.Resize(width, height)); 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 Quality = 80
}, cancellationToken); }, 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); var user = await userRepository.FirstAsync(u => u.Id == userId, cancellationToken: cancellationToken);
user.UpdateAvatar(avatarUrl); user.UpdateAvatar(avatarUrl);
await userRepository.UpdateAsync(user, cancellationToken: cancellationToken); await userRepository.UpdateAsync(user, cancellationToken: cancellationToken);

View File

@ -60,6 +60,19 @@
} }
}, },
"Oms": { "Oms": {
"FileStorage": {
"Provider": "Local",
"Cos": {
"SecretId": "",
"SecretKey": "",
"Region": "ap-guangzhou",
"Bucket": "your-bucket-1250000000",
"BaseUrl": "",
"KeyPrefix": "",
"DurationSecond": 600,
"IsHttps": true
}
},
"TrackService": { "TrackService": {
"Enabled": true, "Enabled": true,
"BaseUrl": "http://api.nanafa.com/track", "BaseUrl": "http://api.nanafa.com/track",

View File

@ -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"
}
}
}