feat: finalize core modules and gateway

This commit is contained in:
2025-11-23 18:53:12 +08:00
parent 429d4fb747
commit ae273e510a
115 changed files with 4695 additions and 223 deletions

View File

@@ -0,0 +1,52 @@
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.Storage.Abstractions;
using TakeoutSaaS.Application.Storage.Contracts;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.AdminApi.Controllers;
/// <summary>
/// 管理后台文件上传。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/admin/v{version:apiVersion}/files")]
public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController
{
private readonly IFileStorageService _fileStorageService = fileStorageService;
/// <summary>
/// 上传图片或文件。
/// </summary>
[HttpPost("upload")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken)
{
if (file == null || file.Length == 0)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
}
if (!UploadFileTypeParser.TryParse(type, out var uploadType))
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
}
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
await using var stream = file.OpenReadStream();
var result = await _fileStorageService.UploadAsync(
new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin),
cancellationToken);
return ApiResponse<FileUploadResponse>.Ok(result);
}
}

View File

@@ -7,9 +7,16 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using TakeoutSaaS.Application.Identity.Extensions;
using TakeoutSaaS.Application.Messaging.Extensions;
using TakeoutSaaS.Application.Sms.Extensions;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Infrastructure.Identity.Extensions;
using TakeoutSaaS.Module.Authorization.Extensions;
using TakeoutSaaS.Module.Dictionary.Extensions;
using TakeoutSaaS.Module.Messaging.Extensions;
using TakeoutSaaS.Module.Scheduler.Extensions;
using TakeoutSaaS.Module.Sms.Extensions;
using TakeoutSaaS.Module.Storage.Extensions;
using TakeoutSaaS.Module.Tenancy.Extensions;
using TakeoutSaaS.Shared.Web.Extensions;
using TakeoutSaaS.Shared.Web.Swagger;
@@ -38,6 +45,13 @@ builder.Services.AddAuthorization();
builder.Services.AddPermissionAuthorization();
builder.Services.AddTenantResolution(builder.Configuration);
builder.Services.AddDictionaryModule(builder.Configuration);
builder.Services.AddStorageModule(builder.Configuration);
builder.Services.AddStorageApplication();
builder.Services.AddSmsModule(builder.Configuration);
builder.Services.AddSmsApplication(builder.Configuration);
builder.Services.AddMessagingModule(builder.Configuration);
builder.Services.AddMessagingApplication();
builder.Services.AddSchedulerModule(builder.Configuration);
var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin");
builder.Services.AddCors(options =>
@@ -56,6 +70,7 @@ app.UseSharedWebCore();
app.UseAuthentication();
app.UseAuthorization();
app.UseSharedSwagger();
app.UseSchedulerDashboard(builder.Configuration);
app.MapControllers();
app.Run();

View File

@@ -15,6 +15,10 @@
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Authorization\TakeoutSaaS.Module.Authorization.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Dictionary\TakeoutSaaS.Module.Dictionary.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Messaging\TakeoutSaaS.Module.Messaging.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Scheduler\TakeoutSaaS.Module.Scheduler.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,142 @@
{
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
}
}
},
"Redis": "localhost:6379,abortConnect=false",
"Identity": {
"Jwt": {
"Issuer": "takeout-saas",
"Audience": "takeout-saas-clients",
"Secret": "ReplaceWithA32CharLongSecretKey_____",
"AccessTokenExpirationMinutes": 120,
"RefreshTokenExpirationMinutes": 10080
},
"LoginRateLimit": {
"WindowSeconds": 60,
"MaxAttempts": 5
},
"RefreshTokenStore": {
"Prefix": "identity:refresh:"
},
"AdminSeed": {
"Users": []
}
},
"Dictionary": {
"Cache": {
"SlidingExpiration": "00:30:00"
}
},
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"IgnoredPaths": [ "/health" ],
"RootDomain": ""
},
"Storage": {
"Provider": "TencentCos",
"CdnBaseUrl": "https://cdn.example.com",
"TencentCos": {
"SecretId": "COS_SECRET_ID",
"SecretKey": "COS_SECRET_KEY",
"Region": "ap-guangzhou",
"Bucket": "takeout-bucket-123456",
"Endpoint": "",
"CdnBaseUrl": "https://cdn.example.com",
"UseHttps": true,
"ForcePathStyle": false
},
"QiniuKodo": {
"AccessKey": "QINIU_ACCESS_KEY",
"SecretKey": "QINIU_SECRET_KEY",
"Bucket": "takeout-files",
"DownloadDomain": "",
"Endpoint": "",
"UseHttps": true,
"SignedUrlExpirationMinutes": 30
},
"AliyunOss": {
"AccessKeyId": "OSS_ACCESS_KEY_ID",
"AccessKeySecret": "OSS_ACCESS_KEY_SECRET",
"Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
"Bucket": "takeout-files",
"CdnBaseUrl": "",
"UseHttps": true
},
"Security": {
"MaxFileSizeBytes": 10485760,
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ],
"AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ],
"DefaultUrlExpirationMinutes": 30,
"EnableRefererValidation": true,
"AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ],
"AntiLeechTokenSecret": "ReplaceWithARandomToken"
}
},
"Sms": {
"Provider": "Tencent",
"DefaultSignName": "外卖SaaS",
"UseMock": true,
"Tencent": {
"SecretId": "TENCENT_SMS_SECRET_ID",
"SecretKey": "TENCENT_SMS_SECRET_KEY",
"SdkAppId": "1400000000",
"SignName": "外卖SaaS",
"Region": "ap-guangzhou",
"Endpoint": "https://sms.tencentcloudapi.com"
},
"Aliyun": {
"AccessKeyId": "ALIYUN_SMS_AK",
"AccessKeySecret": "ALIYUN_SMS_SK",
"Endpoint": "dysmsapi.aliyuncs.com",
"SignName": "外卖SaaS",
"Region": "cn-hangzhou"
},
"SceneTemplates": {
"login": "LOGIN_TEMPLATE_ID",
"register": "REGISTER_TEMPLATE_ID",
"reset": "RESET_TEMPLATE_ID"
},
"VerificationCode": {
"CodeLength": 6,
"ExpireMinutes": 5,
"CooldownSeconds": 60,
"CachePrefix": "sms:code"
}
},
"RabbitMQ": {
"Host": "localhost",
"Port": 5672,
"Username": "admin",
"Password": "password",
"VirtualHost": "/",
"Exchange": "takeout.events",
"ExchangeType": "topic",
"PrefetchCount": 20
},
"Scheduler": {
"ConnectionString": "Host=localhost;Port=5432;Database=takeout_saas_scheduler;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"WorkerCount": 5,
"DashboardEnabled": false,
"DashboardPath": "/hangfire"
}
}

View File

@@ -0,0 +1,52 @@
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.Storage.Abstractions;
using TakeoutSaaS.Application.Storage.Contracts;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
namespace TakeoutSaaS.MiniApi.Controllers;
/// <summary>
/// 小程序文件上传。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/mini/v{version:apiVersion}/files")]
public sealed class FilesController(IFileStorageService fileStorageService) : BaseApiController
{
private readonly IFileStorageService _fileStorageService = fileStorageService;
/// <summary>
/// 上传图片或文件。
/// </summary>
[HttpPost("upload")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken)
{
if (file == null || file.Length == 0)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
}
if (!UploadFileTypeParser.TryParse(type, out var uploadType))
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
}
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
await using var stream = file.OpenReadStream();
var result = await _fileStorageService.UploadAsync(
new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin),
cancellationToken);
return ApiResponse<FileUploadResponse>.Ok(result);
}
}

View File

@@ -1,5 +1,11 @@
using Microsoft.AspNetCore.Cors.Infrastructure;
using Serilog;
using TakeoutSaaS.Application.Messaging.Extensions;
using TakeoutSaaS.Application.Sms.Extensions;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Module.Messaging.Extensions;
using TakeoutSaaS.Module.Sms.Extensions;
using TakeoutSaaS.Module.Storage.Extensions;
using TakeoutSaaS.Module.Tenancy.Extensions;
using TakeoutSaaS.Shared.Web.Extensions;
using TakeoutSaaS.Shared.Web.Swagger;
@@ -22,6 +28,12 @@ builder.Services.AddSharedSwagger(options =>
options.EnableAuthorization = true;
});
builder.Services.AddTenantResolution(builder.Configuration);
builder.Services.AddStorageModule(builder.Configuration);
builder.Services.AddStorageApplication();
builder.Services.AddSmsModule(builder.Configuration);
builder.Services.AddSmsApplication(builder.Configuration);
builder.Services.AddMessagingModule(builder.Configuration);
builder.Services.AddMessagingApplication();
var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini");
builder.Services.AddCors(options =>

View File

@@ -13,6 +13,9 @@
<ItemGroup>
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Web\TakeoutSaaS.Shared.Web.csproj" />
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Messaging\TakeoutSaaS.Module.Messaging.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Sms\TakeoutSaaS.Module.Sms.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Storage\TakeoutSaaS.Module.Storage.csproj" />
<ProjectReference Include="..\..\Modules\TakeoutSaaS.Module.Tenancy\TakeoutSaaS.Module.Tenancy.csproj" />
<ProjectReference Include="..\..\Infrastructure\TakeoutSaaS.Infrastructure\TakeoutSaaS.Infrastructure.csproj" />

View File

@@ -0,0 +1,133 @@
{
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
}
}
},
"Redis": "localhost:6379,abortConnect=false",
"Identity": {
"Jwt": {
"Issuer": "takeout-saas",
"Audience": "takeout-saas-clients",
"Secret": "ReplaceWithA32CharLongSecretKey_____",
"AccessTokenExpirationMinutes": 120,
"RefreshTokenExpirationMinutes": 10080
},
"LoginRateLimit": {
"WindowSeconds": 60,
"MaxAttempts": 5
},
"RefreshTokenStore": {
"Prefix": "identity:refresh:"
}
},
"Dictionary": {
"Cache": {
"SlidingExpiration": "00:30:00"
}
},
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"IgnoredPaths": [ "/health" ],
"RootDomain": ""
},
"Storage": {
"Provider": "TencentCos",
"CdnBaseUrl": "https://cdn.example.com",
"TencentCos": {
"SecretId": "COS_SECRET_ID",
"SecretKey": "COS_SECRET_KEY",
"Region": "ap-guangzhou",
"Bucket": "takeout-bucket-123456",
"Endpoint": "",
"CdnBaseUrl": "https://cdn.example.com",
"UseHttps": true,
"ForcePathStyle": false
},
"QiniuKodo": {
"AccessKey": "QINIU_ACCESS_KEY",
"SecretKey": "QINIU_SECRET_KEY",
"Bucket": "takeout-files",
"DownloadDomain": "",
"Endpoint": "",
"UseHttps": true,
"SignedUrlExpirationMinutes": 30
},
"AliyunOss": {
"AccessKeyId": "OSS_ACCESS_KEY_ID",
"AccessKeySecret": "OSS_ACCESS_KEY_SECRET",
"Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
"Bucket": "takeout-files",
"CdnBaseUrl": "",
"UseHttps": true
},
"Security": {
"MaxFileSizeBytes": 10485760,
"AllowedImageExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif" ],
"AllowedFileExtensions": [ ".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf" ],
"DefaultUrlExpirationMinutes": 30,
"EnableRefererValidation": true,
"AllowedReferers": [ "https://admin.example.com", "https://miniapp.example.com" ],
"AntiLeechTokenSecret": "ReplaceWithARandomToken"
}
},
"Sms": {
"Provider": "Tencent",
"DefaultSignName": "外卖SaaS",
"UseMock": true,
"Tencent": {
"SecretId": "TENCENT_SMS_SECRET_ID",
"SecretKey": "TENCENT_SMS_SECRET_KEY",
"SdkAppId": "1400000000",
"SignName": "外卖SaaS",
"Region": "ap-guangzhou",
"Endpoint": "https://sms.tencentcloudapi.com"
},
"Aliyun": {
"AccessKeyId": "ALIYUN_SMS_AK",
"AccessKeySecret": "ALIYUN_SMS_SK",
"Endpoint": "dysmsapi.aliyuncs.com",
"SignName": "外卖SaaS",
"Region": "cn-hangzhou"
},
"SceneTemplates": {
"login": "LOGIN_TEMPLATE_ID",
"register": "REGISTER_TEMPLATE_ID",
"reset": "RESET_TEMPLATE_ID"
},
"VerificationCode": {
"CodeLength": 6,
"ExpireMinutes": 5,
"CooldownSeconds": 60,
"CachePrefix": "sms:code"
}
},
"RabbitMQ": {
"Host": "localhost",
"Port": 5672,
"Username": "admin",
"Password": "password",
"VirtualHost": "/",
"Exchange": "takeout.events",
"ExchangeType": "topic",
"PrefetchCount": 20
}
}

View File

@@ -0,0 +1,52 @@
{
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_app;Username=app_user;Password=app_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=identity_password;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
}
}
},
"Redis": "localhost:6379,abortConnect=false",
"Identity": {
"Jwt": {
"Issuer": "takeout-saas",
"Audience": "takeout-saas-clients",
"Secret": "ReplaceWithA32CharLongSecretKey_____",
"AccessTokenExpirationMinutes": 120,
"RefreshTokenExpirationMinutes": 10080
},
"LoginRateLimit": {
"WindowSeconds": 60,
"MaxAttempts": 5
},
"RefreshTokenStore": {
"Prefix": "identity:refresh:"
}
},
"Dictionary": {
"Cache": {
"SlidingExpiration": "00:30:00"
}
},
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"IgnoredPaths": [ "/health" ],
"RootDomain": ""
}
}