@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
|
||||
<PropertyGroup>
|
||||
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
|
||||
<PropertyGroup>
|
||||
|
||||
|
||||
</PropertyGroup>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="Tiku.Api/Tiku.Api.csproj" />
|
||||
<Project Path="Tiku.Application/Tiku.Application.csproj" />
|
||||
<Project Path="Tiku.DbMigrator/Tiku.DbMigrator.csproj" />
|
||||
<Project Path="Tiku.Domain/Tiku.Domain.csproj" />
|
||||
<Project Path="Tiku.Infrastructure/Tiku.Infrastructure.csproj" />
|
||||
<Project Path="Tiku.Worker/Tiku.Worker.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="Tiku.IntegrationTests/Tiku.IntegrationTests.csproj" />
|
||||
<Project Path="Tiku.UnitTests/Tiku.UnitTests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="Tiku.Api/Tiku.Api.csproj"/>
|
||||
<Project Path="Tiku.Application/Tiku.Application.csproj"/>
|
||||
<Project Path="Tiku.DbMigrator/Tiku.DbMigrator.csproj"/>
|
||||
<Project Path="Tiku.Domain/Tiku.Domain.csproj"/>
|
||||
<Project Path="Tiku.Infrastructure/Tiku.Infrastructure.csproj"/>
|
||||
<Project Path="Tiku.Worker/Tiku.Worker.csproj"/>
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="Tiku.IntegrationTests/Tiku.IntegrationTests.csproj"/>
|
||||
<Project Path="Tiku.UnitTests/Tiku.UnitTests.csproj"/>
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace Tiku.Api;
|
||||
|
||||
public sealed class ApiProgramMarker;
|
||||
public sealed class ApiProgramMarker;
|
||||
@@ -13,10 +13,7 @@ internal sealed class DevelopmentTenantDomainLifecycleHostedService(
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!options.EnableDevelopmentLocalhostBypass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!options.EnableDevelopmentLocalhostBypass) return;
|
||||
|
||||
var interval = TimeSpan.FromSeconds(Math.Clamp(options.PollSeconds, 1, 3600));
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
@@ -30,9 +27,7 @@ internal sealed class DevelopmentTenantDomainLifecycleHostedService(
|
||||
.GetRequiredService<ITenantDomainLifecycleService>()
|
||||
.ProcessPendingAsync(stoppingToken);
|
||||
if (processed > 0)
|
||||
{
|
||||
logger.LogInformation("Processed {DomainCount} pending Development tenant domains.", processed);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -46,4 +41,4 @@ internal sealed class DevelopmentTenantDomainLifecycleHostedService(
|
||||
await Task.Delay(interval, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,13 @@ namespace Tiku.Api.Caching;
|
||||
internal sealed class TenantPublicCacheInvalidator(IOutputCacheStore outputCacheStore)
|
||||
: ITenantPublicCacheInvalidator
|
||||
{
|
||||
public ValueTask InvalidateCoreAsync(Guid tenantId, CancellationToken cancellationToken) =>
|
||||
outputCacheStore.EvictByTagAsync($"tenant:{tenantId:N}", cancellationToken);
|
||||
|
||||
public async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default) =>
|
||||
public async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await InvalidateCoreAsync(tenantId, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask InvalidateCoreAsync(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
return outputCacheStore.EvictByTagAsync($"tenant:{tenantId:N}", cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,10 @@ internal sealed class TenantPublicOutputCachePolicy : IOutputCachePolicy
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) =>
|
||||
ValueTask.CompletedTask;
|
||||
public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -39,10 +41,8 @@ internal sealed class TenantPublicOutputCachePolicy : IOutputCachePolicy
|
||||
if (response.StatusCode != StatusCodes.Status200OK ||
|
||||
!StringValues.IsNullOrEmpty(response.Headers.SetCookie) ||
|
||||
context.HttpContext.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
context.AllowCacheStorage = false;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ internal static class ApiPresentationExtensions
|
||||
internal static IServiceCollection AddApiPresentation(this IServiceCollection services)
|
||||
{
|
||||
services.AddControllers(options =>
|
||||
options.Conventions.Add(new EndpointAuthorizationMetadataConvention()))
|
||||
options.Conventions.Add(new EndpointAuthorizationMetadataConvention()))
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
@@ -23,4 +23,4 @@ internal static class ApiPresentationExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,4 +42,4 @@ public static class ApplicationBuilderExtensions
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Api.Security;
|
||||
@@ -98,9 +99,7 @@ internal static class AuthenticationExtensions
|
||||
if (string.IsNullOrWhiteSpace(context.Request.Headers.Authorization) &&
|
||||
IsSameOriginBrowserRequest(context.Request) &&
|
||||
context.Request.Cookies.TryGetValue(BrowserAuthOptions.AccessCookie, out var accessToken))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = ValidateTokenAsync,
|
||||
@@ -111,16 +110,11 @@ internal static class AuthenticationExtensions
|
||||
private static bool IsSameOriginBrowserRequest(HttpRequest request)
|
||||
{
|
||||
var source = request.Headers.Origin.ToString();
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
source = request.Headers.Referer.ToString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(source)) source = request.Headers.Referer.ToString();
|
||||
|
||||
if (Uri.TryCreate(source, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return string.Equals(uri.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(uri.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return string.Equals(
|
||||
request.Headers["Sec-Fetch-Site"].ToString(),
|
||||
@@ -154,14 +148,14 @@ internal static class AuthenticationExtensions
|
||||
var tenantId = Guid.TryParse(tenantIdValue, out var parsedTenantId)
|
||||
? parsedTenantId
|
||||
: (Guid?)null;
|
||||
if (realm is null || (realm == AuthRealm.Tenant) != tenantId.HasValue)
|
||||
if (realm is null || realm == AuthRealm.Tenant != tenantId.HasValue)
|
||||
{
|
||||
context.Fail("Token scope and tenant claims are inconsistent.");
|
||||
return;
|
||||
}
|
||||
|
||||
var resolutionOptions = context.HttpContext.RequestServices
|
||||
.GetRequiredService<Microsoft.Extensions.Options.IOptions<TenantResolutionOptions>>().Value;
|
||||
.GetRequiredService<IOptions<TenantResolutionOptions>>().Value;
|
||||
var requestHost = context.HttpContext.Request.Host.Host.Trim().TrimEnd('.');
|
||||
var isPlatformHost = resolutionOptions.PlatformHosts.Any(host =>
|
||||
string.Equals(host.Trim().TrimEnd('.'), requestHost, StringComparison.OrdinalIgnoreCase));
|
||||
@@ -218,10 +212,7 @@ internal static class AuthenticationExtensions
|
||||
|
||||
private static async Task WriteTenantConflictChallengeAsync(JwtBearerChallengeContext context)
|
||||
{
|
||||
if (!context.HttpContext.Items.ContainsKey("tenant_context_conflict"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!context.HttpContext.Items.ContainsKey("tenant_context_conflict")) return;
|
||||
|
||||
context.HandleResponse();
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
@@ -232,4 +223,4 @@ internal static class AuthenticationExtensions
|
||||
Extensions = { ["code"] = "tenant_context_conflict" }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,20 +25,15 @@ internal static class DataProtectionExtensions
|
||||
.Get<DataProtectionKeyRingOptions>() ?? new DataProtectionKeyRingOptions();
|
||||
ApplyEnvironmentOverrides(keyRingOptions, configuration);
|
||||
if (!DataProtectionKeyRingOptions.BeValid(keyRingOptions, requireProtectedKeys))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Data Protection requires an application name and, outside Development, an X509 certificate path.");
|
||||
}
|
||||
|
||||
var dataProtection = services
|
||||
.AddDataProtection()
|
||||
.SetApplicationName(keyRingOptions.ApplicationName.Trim())
|
||||
.PersistKeysToDbContext<TikuDbContext>();
|
||||
var certificate = keyRingOptions.LoadCertificate(requireProtectedKeys);
|
||||
if (certificate is not null)
|
||||
{
|
||||
dataProtection.ProtectKeysWithCertificate(certificate);
|
||||
}
|
||||
if (certificate is not null) dataProtection.ProtectKeysWithCertificate(certificate);
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -54,4 +49,4 @@ internal static class DataProtectionExtensions
|
||||
options.CertificatePassword =
|
||||
configuration["TIKU_DATA_PROTECTION_CERTIFICATE_PASSWORD"] ?? options.CertificatePassword;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
using Serilog;
|
||||
using Tiku.Application;
|
||||
using Tiku.Infrastructure;
|
||||
using Tiku.Infrastructure.Security;
|
||||
using Tiku.Api.Caching;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using System.IO.Compression;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Serilog;
|
||||
using Tiku.Api.Caching;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Tiku.Infrastructure;
|
||||
using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
|
||||
@@ -16,10 +17,10 @@ public static class DependencyInjection
|
||||
public static WebApplicationBuilder AddApiServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddSerilog((services, configuration) => configuration
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext(),
|
||||
preserveStaticLogger: true);
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext(),
|
||||
true);
|
||||
|
||||
builder.Services.AddApiPresentation();
|
||||
builder.Services.AddHealthChecks();
|
||||
@@ -28,11 +29,12 @@ public static class DependencyInjection
|
||||
builder.Services.AddNetworkConfiguration(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddApiRateLimiting(builder.Configuration);
|
||||
|
||||
var connectionString = Options.OptionsValidation.ResolveDatabaseConnectionString(
|
||||
var connectionString = OptionsValidation.ResolveDatabaseConnectionString(
|
||||
builder.Configuration,
|
||||
builder.Environment.IsDevelopment());
|
||||
builder.Services.AddInfrastructure(connectionString);
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
|
||||
var redisConnectionString =
|
||||
builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
|
||||
builder.Services.AddOptions<RedisSecurityConnectionOptions>()
|
||||
.Configure(options => options.ConnectionString = redisConnectionString ?? string.Empty)
|
||||
.Validate(
|
||||
@@ -46,14 +48,10 @@ public static class DependencyInjection
|
||||
"Authorization cache durations must be positive and jitter must be between 0 and 50 percent.")
|
||||
.ValidateOnStart();
|
||||
if (!string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
builder.Services.AddRedisSecurity(redisConnectionString, builder.Environment.EnvironmentName);
|
||||
}
|
||||
else if (builder.Environment.IsProduction())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Redis is required in Production. Configure ConnectionStrings:Redis or REDIS_URL.");
|
||||
}
|
||||
|
||||
builder.Services.AddOutputCache(options =>
|
||||
{
|
||||
@@ -63,13 +61,11 @@ public static class DependencyInjection
|
||||
builder.Services.RemoveAll<ITenantPublicCacheInvalidator>();
|
||||
builder.Services.AddSingleton<ITenantPublicCacheInvalidator, TenantPublicCacheInvalidator>();
|
||||
if (builder.Environment.IsProduction() && !string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
builder.Services.AddStackExchangeRedisOutputCache(options =>
|
||||
{
|
||||
options.Configuration = redisConnectionString;
|
||||
options.InstanceName = $"tiku:{builder.Environment.EnvironmentName.ToLowerInvariant()}:output:";
|
||||
});
|
||||
}
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
@@ -77,7 +73,8 @@ public static class DependencyInjection
|
||||
options.Providers.Add<GzipCompressionProvider>();
|
||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
|
||||
});
|
||||
builder.Services.Configure<BrotliCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
|
||||
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
|
||||
options.Level = CompressionLevel.Fastest);
|
||||
builder.Services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Fastest);
|
||||
builder.Services.AddApiDataProtection(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddExternalServiceOptions(builder.Configuration, builder.Environment);
|
||||
@@ -85,4 +82,4 @@ public static class DependencyInjection
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Infrastructure.Assets;
|
||||
using Tiku.Infrastructure.Commerce;
|
||||
using Tiku.Infrastructure.Storage;
|
||||
using Tiku.Infrastructure.Assets;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
|
||||
@@ -54,21 +56,21 @@ internal static class ExternalServiceOptionsExtensions
|
||||
services.AddOptions<ObjectStorageOptions>()
|
||||
.Validate(
|
||||
options => !environment.IsProduction() ||
|
||||
options.DefaultProvider == Tiku.Application.Storage.ObjectStorageProviders.AliyunOss,
|
||||
options.DefaultProvider == ObjectStorageProviders.AliyunOss,
|
||||
"Production managed storage must use the configured Aliyun OSS provider.")
|
||||
.ValidateOnStart();
|
||||
services.AddOptions<AliyunOssOptions>()
|
||||
.Validate<Microsoft.Extensions.Options.IOptions<ObjectStorageOptions>>(
|
||||
.Validate<IOptions<ObjectStorageOptions>>(
|
||||
(aliyun, storage) =>
|
||||
!environment.IsProduction() ||
|
||||
storage.Value.DefaultProvider != Tiku.Application.Storage.ObjectStorageProviders.AliyunOss ||
|
||||
storage.Value.DefaultProvider != ObjectStorageProviders.AliyunOss ||
|
||||
aliyun.IsConfigured,
|
||||
"Aliyun OSS credentials and region or endpoint are required when it is the default provider.")
|
||||
.ValidateOnStart();
|
||||
services.AddOptions<ClamAvOptions>()
|
||||
.Bind(configuration.GetSection(ClamAvOptions.SectionName))
|
||||
.Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.")
|
||||
.Validate<Microsoft.Extensions.Options.IOptions<ObjectStorageOptions>>(
|
||||
.Validate<IOptions<ObjectStorageOptions>>(
|
||||
(clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes,
|
||||
"ClamAV StreamMaxLength must be greater than or equal to the storage max upload size.")
|
||||
.ValidateOnStart();
|
||||
@@ -104,8 +106,10 @@ internal static class ExternalServiceOptionsExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
private static string[] SplitLegacyList(string? value, string[] fallback) =>
|
||||
string.IsNullOrWhiteSpace(value)
|
||||
private static string[] SplitLegacyList(string? value, string[] fallback)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? fallback
|
||||
: value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ using System.Net;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Tiku.Api.Background;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
|
||||
@@ -17,7 +17,8 @@ internal static class NetworkConfigurationExtensions
|
||||
services.AddOptions<TenantResolutionOptions>()
|
||||
.Bind(configuration.GetSection(TenantResolutionOptions.SectionName))
|
||||
.Validate(
|
||||
options => OptionsValidation.BeValidTenantResolutionOptions(options, configuration, environment.IsProduction()),
|
||||
options => OptionsValidation.BeValidTenantResolutionOptions(options, configuration,
|
||||
environment.IsProduction()),
|
||||
"Production requires formal platform hosts, non-wildcard AllowedHosts, and trusted proxy addresses.")
|
||||
.ValidateOnStart();
|
||||
services.Configure<ForwardedHeadersOptions>(options =>
|
||||
@@ -34,22 +35,15 @@ internal static class NetworkConfigurationExtensions
|
||||
.GetSection(TenantResolutionOptions.SectionName)
|
||||
.Get<TenantResolutionOptions>() ?? new TenantResolutionOptions();
|
||||
foreach (var address in resolution.TrustedProxyAddresses)
|
||||
{
|
||||
if (IPAddress.TryParse(address, out var proxy))
|
||||
{
|
||||
options.KnownProxies.Add(proxy);
|
||||
}
|
||||
}
|
||||
});
|
||||
services.AddOptions<DomainLifecycleOptions>()
|
||||
.Bind(configuration.GetSection("TenantDomains"))
|
||||
.Validate(options => environment.IsDevelopment() || !options.EnableDevelopmentLocalhostBypass,
|
||||
"The .localhost domain lifecycle bypass can only be enabled in Development.")
|
||||
.ValidateOnStart();
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
services.AddHostedService<DevelopmentTenantDomainLifecycleHostedService>();
|
||||
}
|
||||
if (environment.IsDevelopment()) services.AddHostedService<DevelopmentTenantDomainLifecycleHostedService>();
|
||||
services.AddOptions<TenantProvisioningOptions>()
|
||||
.Bind(configuration.GetSection(TenantProvisioningOptions.SectionName))
|
||||
.Validate(options => !string.IsNullOrWhiteSpace(options.DefaultBaseOfferingCode) &&
|
||||
@@ -57,17 +51,15 @@ internal static class NetworkConfigurationExtensions
|
||||
options.OwnerActivationMinutes is >= 5 and <= 1440 &&
|
||||
options.OwnerActivationUrlTemplate.Contains("{host}", StringComparison.Ordinal) &&
|
||||
Uri.TryCreate(
|
||||
options.OwnerActivationUrlTemplate.Replace("{host}", "tenant.example.com", StringComparison.Ordinal),
|
||||
options.OwnerActivationUrlTemplate.Replace("{host}", "tenant.example.com",
|
||||
StringComparison.Ordinal),
|
||||
UriKind.Absolute,
|
||||
out var activationOrigin) &&
|
||||
activationOrigin.Scheme == Uri.UriSchemeHttps &&
|
||||
ValidateDevelopmentActivationTemplate(options, environment.IsDevelopment()),
|
||||
"Tenant provisioning requires a default offering code, valid trial/activation durations, an HTTPS owner activation URL template, and permits an HTTP template only for Development .localhost sites.")
|
||||
.ValidateOnStart();
|
||||
if (environment.IsProduction())
|
||||
{
|
||||
services.AddHostedService<TenantProvisioningStartupValidator>();
|
||||
}
|
||||
if (environment.IsProduction()) services.AddHostedService<TenantProvisioningStartupValidator>();
|
||||
|
||||
services.AddOptions<CorsOptions>()
|
||||
.Bind(configuration.GetSection(CorsOptions.SectionName))
|
||||
@@ -90,19 +82,13 @@ internal static class NetworkConfigurationExtensions
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
if (origins.Length > 0)
|
||||
{
|
||||
policy.WithOrigins(origins);
|
||||
}
|
||||
if (origins.Length > 0) policy.WithOrigins(origins);
|
||||
|
||||
policy
|
||||
.WithHeaders(corsOptions.AllowedHeaders)
|
||||
.WithMethods(corsOptions.AllowedMethods);
|
||||
|
||||
if (corsOptions.AllowCredentials)
|
||||
{
|
||||
policy.AllowCredentials();
|
||||
}
|
||||
if (corsOptions.AllowCredentials) policy.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,10 +99,7 @@ internal static class NetworkConfigurationExtensions
|
||||
TenantProvisioningOptions options,
|
||||
bool isDevelopment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.DevelopmentLocalhostOwnerActivationUrlTemplate))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(options.DevelopmentLocalhostOwnerActivationUrlTemplate)) return true;
|
||||
|
||||
return isDevelopment &&
|
||||
options.DevelopmentLocalhostOwnerActivationUrlTemplate.Contains("{host}", StringComparison.Ordinal) &&
|
||||
@@ -128,4 +111,4 @@ internal static class NetworkConfigurationExtensions
|
||||
(developmentOrigin.Scheme == Uri.UriSchemeHttp ||
|
||||
developmentOrigin.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,31 +18,30 @@ internal static class ObservabilityExtensions
|
||||
|
||||
services.AddOpenTelemetry()
|
||||
.ConfigureResource(resource => resource.AddService(
|
||||
serviceName: environment.ApplicationName,
|
||||
environment.ApplicationName,
|
||||
serviceVersion: typeof(ObservabilityExtensions).Assembly.GetName().Version?.ToString()))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation(options =>
|
||||
options.Filter = context => !context.Request.Path.StartsWithSegments("/api/system/health"))
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddSource("Npgsql")
|
||||
.ApplyIf(hasOtlpEndpoint, builder => builder.AddOtlpExporter(options => options.Endpoint = endpointUri!)))
|
||||
.ApplyIf(hasOtlpEndpoint,
|
||||
builder => builder.AddOtlpExporter(options => options.Endpoint = endpointUri!)))
|
||||
.WithMetrics(metrics => metrics
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddMeter(DatabasePerformanceTelemetry.MeterName, WorkerTelemetry.MeterName,
|
||||
AuthorizationCacheTelemetry.MeterName, "Tiku.Security.Redis", "Tiku.Learning", "Npgsql")
|
||||
.ApplyIf(hasOtlpEndpoint, builder => builder.AddOtlpExporter(options => options.Endpoint = endpointUri!)));
|
||||
.ApplyIf(hasOtlpEndpoint,
|
||||
builder => builder.AddOtlpExporter(options => options.Endpoint = endpointUri!)));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static TBuilder ApplyIf<TBuilder>(this TBuilder builder, bool condition, Action<TBuilder> configure)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
configure(builder);
|
||||
}
|
||||
if (condition) configure(builder);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Tiku.Api.Middleware;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Configuration;
|
||||
@@ -34,7 +33,6 @@ internal static class RateLimitingExtensions
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||
{
|
||||
var partitionKey =
|
||||
@@ -49,7 +47,6 @@ internal static class RateLimitingExtensions
|
||||
rateLimitOptions.QueueLimit,
|
||||
rateLimitOptions.WindowSeconds));
|
||||
});
|
||||
}
|
||||
|
||||
options.AddPolicy(
|
||||
AuthRateLimitPolicies.Password,
|
||||
@@ -76,8 +73,9 @@ internal static class RateLimitingExtensions
|
||||
private static FixedWindowRateLimiterOptions CreateLimiterOptions(
|
||||
int permitLimit,
|
||||
int queueLimit,
|
||||
int windowSeconds) =>
|
||||
new()
|
||||
int windowSeconds)
|
||||
{
|
||||
return new FixedWindowRateLimiterOptions
|
||||
{
|
||||
AutoReplenishment = true,
|
||||
PermitLimit = permitLimit,
|
||||
@@ -85,15 +83,14 @@ internal static class RateLimitingExtensions
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
Window = TimeSpan.FromSeconds(windowSeconds)
|
||||
};
|
||||
}
|
||||
|
||||
private static async ValueTask WriteRateLimitProblemAsync(
|
||||
OnRejectedContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
||||
{
|
||||
context.HttpContext.Response.Headers.RetryAfter = ((int)retryAfter.TotalSeconds).ToString();
|
||||
}
|
||||
|
||||
var problem = new ProblemDetails
|
||||
{
|
||||
@@ -107,4 +104,4 @@ internal static class RateLimitingExtensions
|
||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
await context.HttpContext.Response.WriteAsJsonAsync(problem, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,13 +41,14 @@ internal sealed class TenantProvisioningStartupValidator(
|
||||
cancellationToken);
|
||||
|
||||
if (!available)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TenantProvisioning:DefaultBaseOfferingCode '{offeringCode}' does not resolve to an effective published base offering version.");
|
||||
}
|
||||
|
||||
logger.LogInformation("Validated default tenant provisioning offering {OfferingCode}", offeringCode);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,25 @@ using Tiku.Domain.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 资产访问签名查询参数。
|
||||
/// 资产访问签名查询参数。
|
||||
/// </summary>
|
||||
public sealed class AssetAccessQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期秒数。
|
||||
/// 有效期秒数。
|
||||
/// </summary>
|
||||
[Range(1, 7200)]
|
||||
public int? ExpiresInSeconds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资产访问签名响应。
|
||||
/// 资产访问签名响应。
|
||||
/// </summary>
|
||||
public sealed record AssetAccessResponseDto(
|
||||
ContentAssetAccessSummaryDto Item,
|
||||
@@ -47,7 +47,7 @@ public sealed record AssetAccessResponseDto(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内容资产访问摘要。
|
||||
/// 内容资产访问摘要。
|
||||
/// </summary>
|
||||
public sealed record ContentAssetAccessSummaryDto(
|
||||
Guid Id,
|
||||
@@ -76,7 +76,7 @@ public sealed record ContentAssetAccessSummaryDto(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资产访问Principal请求 DTO。
|
||||
/// 资产访问Principal请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record AssetAccessPrincipalDto(
|
||||
Guid? UserId,
|
||||
@@ -84,7 +84,7 @@ public sealed record AssetAccessPrincipalDto(
|
||||
bool HasSvip);
|
||||
|
||||
/// <summary>
|
||||
/// SignedStorageUrl请求 DTO。
|
||||
/// SignedStorageUrl请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record SignedStorageUrlDto(
|
||||
string Provider,
|
||||
@@ -110,4 +110,4 @@ public sealed record SignedStorageUrlDto(
|
||||
(int)signedUrl.ExpiresIn.TotalSeconds,
|
||||
signedUrl.SignatureMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,82 +4,82 @@ using Tiku.Application.Assets;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 资产查询参数。
|
||||
/// 资产查询参数。
|
||||
/// </summary>
|
||||
public sealed class AssetQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产类型。
|
||||
/// 资产类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产键。
|
||||
/// 资产键。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? AssetKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含锁定资源。
|
||||
/// 是否包含锁定资源。
|
||||
/// </summary>
|
||||
public bool IncludeLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含停用数据。
|
||||
/// 是否包含停用数据。
|
||||
/// </summary>
|
||||
public bool IncludeInactive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -102,4 +102,4 @@ public sealed class AssetQueryDto
|
||||
IncludeInactive,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,128 +6,128 @@ using Tiku.Domain.Common;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 创建资产上传签名请求。
|
||||
/// 创建资产上传签名请求。
|
||||
/// </summary>
|
||||
public sealed class AssetUploadSignDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产键。
|
||||
/// 资产键。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? AssetKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(500)]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// MIME 类型。
|
||||
/// MIME 类型。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string MimeType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小,单位为字节。
|
||||
/// 文件大小,单位为字节。
|
||||
/// </summary>
|
||||
[Range(0, long.MaxValue)]
|
||||
public long? FileSizeBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 校验值。
|
||||
/// SHA-256 校验值。
|
||||
/// </summary>
|
||||
[RegularExpression("^[A-Fa-f0-9]{64}$")]
|
||||
public string? ChecksumSha256 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产类型。
|
||||
/// 资产类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可见性。
|
||||
/// 可见性。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否公开。
|
||||
/// 是否公开。
|
||||
/// </summary>
|
||||
public bool? IsPublic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶。
|
||||
/// 存储桶。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Bucket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对象存储键。
|
||||
/// 对象存储键。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? ObjectKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期秒数。
|
||||
/// 有效期秒数。
|
||||
/// </summary>
|
||||
[Range(1, 3600)]
|
||||
public int? ExpiresInSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -159,30 +159,30 @@ public sealed class AssetUploadSignDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确认资产上传请求。
|
||||
/// 确认资产上传请求。
|
||||
/// </summary>
|
||||
public sealed class AssetUploadConfirmDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME 类型。
|
||||
/// MIME 类型。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小,单位为字节。
|
||||
/// 文件大小,单位为字节。
|
||||
/// </summary>
|
||||
[Range(0, long.MaxValue)]
|
||||
public long? FileSizeBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 校验值。
|
||||
/// SHA-256 校验值。
|
||||
/// </summary>
|
||||
[RegularExpression("^[A-Fa-f0-9]{64}$")]
|
||||
public string? ChecksumSha256 { get; set; }
|
||||
@@ -194,116 +194,142 @@ public sealed class AssetUploadConfirmDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新内容资产请求。
|
||||
/// 新增或更新内容资产请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertAssetDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 历史系统 ID。
|
||||
/// 历史系统 ID。
|
||||
/// </summary>
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产键。
|
||||
/// 资产键。
|
||||
/// </summary>
|
||||
public string? AssetKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string? FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CDN 地址。
|
||||
/// CDN 地址。
|
||||
/// </summary>
|
||||
public string? CdnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否公开。
|
||||
/// 是否公开。
|
||||
/// </summary>
|
||||
public bool? IsPublic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产类型。
|
||||
/// 资产类型。
|
||||
/// </summary>
|
||||
public string? AssetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可见性。
|
||||
/// 可见性。
|
||||
/// </summary>
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶。
|
||||
/// 存储桶。
|
||||
/// </summary>
|
||||
public string? Bucket { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对象存储键。
|
||||
/// 对象存储键。
|
||||
/// </summary>
|
||||
public string? ObjectKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME 类型。
|
||||
/// MIME 类型。
|
||||
/// </summary>
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小,单位为字节。
|
||||
/// 文件大小,单位为字节。
|
||||
/// </summary>
|
||||
public long? FileSizeBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 校验值。
|
||||
/// SHA-256 校验值。
|
||||
/// </summary>
|
||||
public string? ChecksumSha256 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览地址。
|
||||
/// 预览地址。
|
||||
/// </summary>
|
||||
public string? PreviewUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览对象存储键。
|
||||
/// 预览对象存储键。
|
||||
/// </summary>
|
||||
public string? PreviewObjectKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问规则。
|
||||
/// 访问规则。
|
||||
/// </summary>
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -341,18 +367,18 @@ public sealed class UpsertAssetDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建资产访问签名请求。
|
||||
/// 创建资产访问签名请求。
|
||||
/// </summary>
|
||||
public sealed class AssetAccessSignDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期秒数。
|
||||
/// 有效期秒数。
|
||||
/// </summary>
|
||||
[Range(60, 3600)]
|
||||
public int? ExpiresInSeconds { get; set; }
|
||||
@@ -364,62 +390,62 @@ public sealed class AssetAccessSignDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资产管理查询参数。
|
||||
/// 资产管理查询参数。
|
||||
/// </summary>
|
||||
public sealed class AssetManagementQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产类型。
|
||||
/// 资产类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上传状态。
|
||||
/// 上传状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? UploadStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 安全扫描状态。
|
||||
/// 安全扫描状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? SecurityScanStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -441,21 +467,22 @@ public sealed class AssetManagementQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资产事件查询参数。
|
||||
/// 资产事件查询参数。
|
||||
/// </summary>
|
||||
public sealed class AssetEventQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -467,30 +494,30 @@ public sealed class AssetEventQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入任务查询参数。
|
||||
/// 导入任务查询参数。
|
||||
/// </summary>
|
||||
public sealed class ImportJobQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导入Type。
|
||||
/// 导入Type。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? ImportType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源格式。
|
||||
/// 来源格式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? SourceFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -499,4 +526,4 @@ public sealed class ImportJobQueryDto
|
||||
{
|
||||
return new ImportJobFilter(Status, ImportType, SourceFormat, Limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,39 @@ using Tiku.Domain.Tenancy;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// PasswordLogin请求 DTO。
|
||||
/// PasswordLogin请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class PasswordLoginDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 认证域。
|
||||
/// 认证域。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public AuthRealm? Realm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账号标识。
|
||||
/// 账号标识。
|
||||
/// </summary>
|
||||
[StringLength(320)]
|
||||
[Description("账号标识。tenant 可使用手机号,platform 可使用邮箱或用户名。")]
|
||||
public string? Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兼容手机号字段;新客户端应使用 identifier。
|
||||
/// 兼容手机号字段;新客户端应使用 identifier。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
[Description("兼容手机号字段;新客户端应使用 identifier。")]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密码。
|
||||
/// 密码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 8)]
|
||||
@@ -46,24 +47,25 @@ public sealed class PasswordLoginDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SmsLogin请求 DTO。
|
||||
/// SmsLogin请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class SmsLoginDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 认证域。
|
||||
/// 认证域。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public AuthRealm? Realm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
@@ -71,7 +73,7 @@ public sealed class SmsLoginDto
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(12, MinimumLength = 4)]
|
||||
@@ -80,55 +82,56 @@ public sealed class SmsLoginDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送短信验证码请求。
|
||||
/// 发送短信验证码请求。
|
||||
/// </summary>
|
||||
public sealed class SendSmsCodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 认证域。
|
||||
/// 认证域。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public AuthRealm? Realm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 设备 ID。
|
||||
/// 设备 ID。
|
||||
/// </summary>
|
||||
[StringLength(256)]
|
||||
public string? DeviceId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// O认证编码请求 DTO。
|
||||
/// O认证编码请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class OAuthCodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 认证域。
|
||||
/// 认证域。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public AuthRealm? Realm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[Description("平台控制域名登录时使用的租户代码;自定义域名登录可省略。")]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(512)]
|
||||
@@ -136,13 +139,13 @@ public sealed class OAuthCodeDto
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 公开用户资料。
|
||||
/// 公开用户资料。
|
||||
/// </summary>
|
||||
[Description("客户端可提供的公开用户资料,不允许包含 token/secret。当前后端先保留模板字段,后续按业务需要逐步使用。")]
|
||||
public Dictionary<string, object?>? Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言。
|
||||
/// 语言。
|
||||
/// </summary>
|
||||
[StringLength(20)]
|
||||
[Description("微信用户资料语言,例如 zh_CN。当前微信小程序登录不会使用该字段。")]
|
||||
@@ -150,12 +153,12 @@ public sealed class OAuthCodeDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新会话请求 DTO。
|
||||
/// 刷新会话请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class RefreshSessionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 刷新令牌。
|
||||
/// 刷新令牌。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(2048)]
|
||||
@@ -164,42 +167,42 @@ public sealed class RefreshSessionDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticated用户请求 DTO。
|
||||
/// Authenticated用户请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class AuthenticatedUserDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
public string? Phone { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱。
|
||||
/// 邮箱。
|
||||
/// </summary>
|
||||
public string? Email { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 认证域。
|
||||
/// 认证域。
|
||||
/// </summary>
|
||||
public AuthRealm Realm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户成员摘要。
|
||||
/// 租户成员摘要。
|
||||
/// </summary>
|
||||
public TenantMembershipSummary? Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问令牌和刷新令牌。
|
||||
/// 访问令牌和刷新令牌。
|
||||
/// </summary>
|
||||
public AuthTokenPair Tokens { get; init; } = default!;
|
||||
|
||||
@@ -219,50 +222,56 @@ public sealed class AuthenticatedUserDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录认证结果。
|
||||
/// 登录认证结果。
|
||||
/// </summary>
|
||||
public sealed class AuthenticationResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public AuthenticationStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户信息。
|
||||
/// 用户信息。
|
||||
/// </summary>
|
||||
public AuthenticatedUserDto? User { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 挑战令牌。
|
||||
/// 挑战令牌。
|
||||
/// </summary>
|
||||
public string? ChallengeToken { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 挑战令牌过期时间。
|
||||
/// 挑战令牌过期时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? ChallengeExpiresAt { get; init; }
|
||||
|
||||
public static AuthenticationResultDto FromApplication(AuthenticationResult result) => new()
|
||||
public static AuthenticationResultDto FromApplication(AuthenticationResult result)
|
||||
{
|
||||
Status = result.Status,
|
||||
User = result.User is null ? null : AuthenticatedUserDto.FromApplication(result.User),
|
||||
ChallengeToken = result.ChallengeToken,
|
||||
ChallengeExpiresAt = result.ChallengeExpiresAt
|
||||
};
|
||||
return new AuthenticationResultDto
|
||||
{
|
||||
Status = result.Status,
|
||||
User = result.User is null ? null : AuthenticatedUserDto.FromApplication(result.User),
|
||||
ChallengeToken = result.ChallengeToken,
|
||||
ChallengeExpiresAt = result.ChallengeExpiresAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 首次登录必改密码请求。
|
||||
/// 首次登录必改密码请求。
|
||||
/// </summary>
|
||||
public sealed class RequiredPasswordChangeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 挑战令牌。
|
||||
/// 挑战令牌。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(2048)]
|
||||
public string ChallengeToken { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 新密码。
|
||||
/// 新密码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 8)]
|
||||
@@ -270,7 +279,7 @@ public sealed class RequiredPasswordChangeDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求租户短信密码重置验证码。
|
||||
/// 请求租户短信密码重置验证码。
|
||||
/// </summary>
|
||||
public sealed class PasswordResetSmsSendDto
|
||||
{
|
||||
@@ -279,7 +288,8 @@ public sealed class PasswordResetSmsSendDto
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>绑定到账号的手机号。</summary>
|
||||
[Required, StringLength(32)]
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>客户端设备标识,用于安全频控。</summary>
|
||||
@@ -288,7 +298,7 @@ public sealed class PasswordResetSmsSendDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用短信验证码重置租户账号密码。
|
||||
/// 使用短信验证码重置租户账号密码。
|
||||
/// </summary>
|
||||
public sealed class PasswordResetDto
|
||||
{
|
||||
@@ -297,48 +307,55 @@ public sealed class PasswordResetDto
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>绑定到账号的手机号。</summary>
|
||||
[Required, StringLength(32)]
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>短信验证码。</summary>
|
||||
[Required, StringLength(12, MinimumLength = 4)]
|
||||
[Required]
|
||||
[StringLength(12, MinimumLength = 4)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>符合当前密码策略的新密码。</summary>
|
||||
[Required, StringLength(128, MinimumLength = 8)]
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 8)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 已登录用户修改密码。
|
||||
/// 已登录用户修改密码。
|
||||
/// </summary>
|
||||
public sealed class AuthenticatedPasswordChangeDto
|
||||
{
|
||||
/// <summary>当前密码。</summary>
|
||||
[Required, StringLength(128, MinimumLength = 1)]
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 1)]
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>符合当前密码策略的新密码。</summary>
|
||||
[Required, StringLength(128, MinimumLength = 8)]
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 8)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员为用户设置一次性临时密码。
|
||||
/// 管理员为用户设置一次性临时密码。
|
||||
/// </summary>
|
||||
public sealed class AdministrativePasswordResetDto
|
||||
{
|
||||
/// <summary>符合当前密码策略的临时密码。</summary>
|
||||
[Required, StringLength(128, MinimumLength = 12)]
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 12)]
|
||||
public string TemporaryPassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>审计原因。</summary>
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完成租户负责人一次性激活。
|
||||
/// 完成租户负责人一次性激活。
|
||||
/// </summary>
|
||||
public sealed class CompleteOwnerActivationDto
|
||||
{
|
||||
@@ -347,12 +364,17 @@ public sealed class CompleteOwnerActivationDto
|
||||
public Guid ActivationId { get; set; }
|
||||
|
||||
/// <summary>只显示一次的激活令牌。</summary>
|
||||
[Required, StringLength(512, MinimumLength = 32)]
|
||||
[Required]
|
||||
[StringLength(512, MinimumLength = 32)]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>符合当前密码策略的新密码。</summary>
|
||||
[Required, StringLength(128, MinimumLength = 8)]
|
||||
[Required]
|
||||
[StringLength(128, MinimumLength = 8)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
|
||||
public CompleteOwnerActivationRequest ToRequest() => new(ActivationId, Token, NewPassword);
|
||||
}
|
||||
public CompleteOwnerActivationRequest ToRequest()
|
||||
{
|
||||
return new CompleteOwnerActivationRequest(ActivationId, Token, NewPassword);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Jobs;
|
||||
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 创建租户后台任务请求。
|
||||
/// 创建租户后台任务请求。
|
||||
/// </summary>
|
||||
public sealed class CreateBackgroundJobDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务类型。
|
||||
/// 任务类型。
|
||||
/// </summary>
|
||||
public string JobType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务载荷。
|
||||
/// 任务载荷。
|
||||
/// </summary>
|
||||
public JsonElement Payload { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计划运行时间。
|
||||
/// 计划运行时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? RunAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大重试次数。
|
||||
/// 最大重试次数。
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>同租户同任务类型内的可选幂等键。</summary>
|
||||
public string? IdempotencyKey { get; set; }
|
||||
|
||||
@@ -43,7 +48,7 @@ public sealed class CreateBackgroundJobDto
|
||||
public sealed class CancelBackgroundJobDto
|
||||
{
|
||||
/// <summary>取消原因。</summary>
|
||||
[System.ComponentModel.DataAnnotations.Required]
|
||||
[System.ComponentModel.DataAnnotations.StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -5,32 +5,37 @@ using Tiku.Domain.Operations;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 创建或更新后台角色请求。
|
||||
/// 创建或更新后台角色请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertBackofficeRoleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public BackendRoleStatus Status { get; set; } = BackendRoleStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据范围配置。
|
||||
/// 数据范围配置。
|
||||
/// </summary>
|
||||
public JsonElement? DataScope { get; set; }
|
||||
|
||||
@@ -41,16 +46,17 @@ public sealed class UpsertBackofficeRoleDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换角色权限绑定请求。
|
||||
/// 替换角色权限绑定请求。
|
||||
/// </summary>
|
||||
public sealed class ReplaceRoleBindingsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限编码列表。
|
||||
/// 权限编码列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> PermissionCodes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 菜单编码列表。
|
||||
/// 菜单编码列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> MenuCodes { get; set; } = [];
|
||||
|
||||
@@ -61,12 +67,12 @@ public sealed class ReplaceRoleBindingsDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换用户角色请求。
|
||||
/// 替换用户角色请求。
|
||||
/// </summary>
|
||||
public sealed class ReplaceUserRolesDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色 ID 列表。
|
||||
/// 角色 ID 列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Guid> RoleIds { get; set; } = [];
|
||||
|
||||
@@ -74,4 +80,4 @@ public sealed class ReplaceUserRolesDto
|
||||
{
|
||||
return new ReplaceUserRolesCommand(userId, RoleIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,67 +4,67 @@ using Tiku.Application.Catalog;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 目录查询参数。
|
||||
/// 目录查询参数。
|
||||
/// </summary>
|
||||
public sealed class CatalogQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ModuleId。
|
||||
/// ModuleId。
|
||||
/// </summary>
|
||||
public Guid? ModuleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
|
||||
public string? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 院校 ID。
|
||||
/// 院校 ID。
|
||||
/// </summary>
|
||||
public Guid? SchoolId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 专业 ID。
|
||||
/// 专业 ID。
|
||||
/// </summary>
|
||||
public Guid? MajorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
/// 类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 2000)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -90,4 +90,4 @@ public sealed class CatalogQueryDto
|
||||
Type,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,47 @@ using Tiku.Application.Commerce;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 创建交易订单请求 DTO。
|
||||
/// 创建交易订单请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreateCommerceOrderDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 套餐 ID。
|
||||
/// 套餐 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid PlanId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数量。
|
||||
/// 数量。
|
||||
/// </summary>
|
||||
[Range(1, 99)]
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? PayMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付渠道。
|
||||
/// 支付渠道。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? PayProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优惠券码。
|
||||
/// 优惠券码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? CouponCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优惠券领取记录 ID。
|
||||
/// 优惠券领取记录 ID。
|
||||
/// </summary>
|
||||
public Guid? CouponRedemptionId { get; set; }
|
||||
|
||||
@@ -62,63 +62,63 @@ public sealed class CreateCommerceOrderDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易订单查询参数。
|
||||
/// 交易订单查询参数。
|
||||
/// </summary>
|
||||
public sealed class CommerceOrderQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 100)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建交易支付请求 DTO。
|
||||
/// 创建交易支付请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreateCommercePaymentDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号。
|
||||
/// 订单号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string Provider { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 方式。
|
||||
/// 方式。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string Method { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 微信或支付渠道 openid。
|
||||
/// 微信或支付渠道 openid。
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? OpenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付完成返回地址。
|
||||
/// 支付完成返回地址。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付取消返回地址。
|
||||
/// 支付取消返回地址。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? QuitUrl { get; set; }
|
||||
@@ -136,18 +136,18 @@ public sealed class CreateCommercePaymentDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 交易优惠券查询参数。
|
||||
/// 交易优惠券查询参数。
|
||||
/// </summary>
|
||||
public sealed class CommerceCouponQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 100)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
@@ -159,12 +159,12 @@ public sealed class CommerceCouponQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取交易优惠券请求 DTO。
|
||||
/// 领取交易优惠券请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ClaimCommerceCouponDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 优惠券码。
|
||||
/// 优惠券码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
@@ -177,35 +177,35 @@ public sealed class ClaimCommerceCouponDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验交易优惠券请求 DTO。
|
||||
/// 校验交易优惠券请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CheckCommerceCouponDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 优惠券码。
|
||||
/// 优惠券码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? CouponCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优惠券领取记录 ID。
|
||||
/// 优惠券领取记录 ID。
|
||||
/// </summary>
|
||||
public Guid? CouponRedemptionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 套餐 ID。
|
||||
/// 套餐 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid PlanId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数量。
|
||||
/// 数量。
|
||||
/// </summary>
|
||||
[Range(1, 99)]
|
||||
public int Quantity { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
@@ -213,4 +213,4 @@ public sealed class CheckCommerceCouponDto
|
||||
{
|
||||
return new CheckCommerceCouponCommand(CouponCode, CouponRedemptionId, PlanId, Quantity, RegionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,293 +6,373 @@ using Tiku.Application.Growth;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 保存佣金配置请求。
|
||||
/// 保存佣金配置请求。
|
||||
/// </summary>
|
||||
public sealed class UpdateCommissionSettingsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认佣金比例。
|
||||
/// 默认佣金比例。
|
||||
/// </summary>
|
||||
[Range(0, 1)]
|
||||
public decimal? DefaultRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最低结算金额,单位为分。
|
||||
/// 最低结算金额,单位为分。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? MinSettlementCents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结算周期。
|
||||
/// 结算周期。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? SettlementCycle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置内容。
|
||||
/// 配置内容。
|
||||
/// </summary>
|
||||
public JsonElement? Config { get; set; }
|
||||
public UpdateCommissionSettingsCommand ToCommand() => new(DefaultRate, MinSettlementCents, SettlementCycle, Config);
|
||||
|
||||
public UpdateCommissionSettingsCommand ToCommand()
|
||||
{
|
||||
return new UpdateCommissionSettingsCommand(DefaultRate, MinSettlementCents, SettlementCycle, Config);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调整成员佣金比例请求。
|
||||
/// 调整成员佣金比例请求。
|
||||
/// </summary>
|
||||
public sealed class UpdateMemberCommissionRateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成员佣金比例。
|
||||
/// 成员佣金比例。
|
||||
/// </summary>
|
||||
[Range(0, 1)]
|
||||
public decimal? CommissionRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 佣金扩展配置。
|
||||
/// 佣金扩展配置。
|
||||
/// </summary>
|
||||
public JsonElement? CommissionConfig { get; set; }
|
||||
public UpdateMemberCommissionRateCommand ToCommand() => new(UserId, CommissionRate, CommissionConfig);
|
||||
|
||||
public UpdateMemberCommissionRateCommand ToCommand()
|
||||
{
|
||||
return new UpdateMemberCommissionRateCommand(UserId, CommissionRate, CommissionConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 佣金统计周期查询参数。
|
||||
/// 佣金统计周期查询参数。
|
||||
/// </summary>
|
||||
public class CommissionPeriodQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
public Guid? ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
public CommissionPeriodQuery ToQuery() => new(ParseDate(StartDate), ParseDate(EndDate), ReferrerUserId, Limit);
|
||||
protected static DateOnly? ParseDate(string? value) => string.IsNullOrWhiteSpace(value) ? null : DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
public CommissionPeriodQuery ToQuery()
|
||||
{
|
||||
return new CommissionPeriodQuery(ParseDate(StartDate), ParseDate(EndDate), ReferrerUserId, Limit);
|
||||
}
|
||||
|
||||
protected static DateOnly? ParseDate(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? null
|
||||
: DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 佣金结算单查询参数。
|
||||
/// 佣金结算单查询参数。
|
||||
/// </summary>
|
||||
public sealed class CommissionSettlementsQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
public Guid? ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
public CommissionSettlementQuery ToQuery() => new(Status, ReferrerUserId, Limit);
|
||||
|
||||
public CommissionSettlementQuery ToQuery()
|
||||
{
|
||||
return new CommissionSettlementQuery(Status, ReferrerUserId, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成佣金结算单请求 DTO。
|
||||
/// 生成佣金结算单请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class GenerateCommissionSettlementDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string StartDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string EndDate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
public GenerateCommissionSettlementCommand ToCommand() => new(Parse(StartDate), Parse(EndDate), ReferrerUserId, Status, Remark, Metadata);
|
||||
private static DateOnly Parse(string value) => DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
|
||||
public GenerateCommissionSettlementCommand ToCommand()
|
||||
{
|
||||
return new GenerateCommissionSettlementCommand(Parse(StartDate), Parse(EndDate), ReferrerUserId, Status, Remark,
|
||||
Metadata);
|
||||
}
|
||||
|
||||
private static DateOnly Parse(string value)
|
||||
{
|
||||
return DateOnly.ParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新佣金结算单状态请求 DTO。
|
||||
/// 更新佣金结算单状态请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpdateCommissionSettlementStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 结算单 ID。
|
||||
/// 结算单 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid SettlementId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核备注。
|
||||
/// 审核备注。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? ReviewNote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? PaymentMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款账号。
|
||||
/// 收款账号。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? PaymentAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
public UpdateCommissionSettlementStatusCommand ToCommand() => new(SettlementId, Status, ReviewNote, PaymentMethod, PaymentAccount, Metadata);
|
||||
|
||||
public UpdateCommissionSettlementStatusCommand ToCommand()
|
||||
{
|
||||
return new UpdateCommissionSettlementStatusCommand(SettlementId, Status, ReviewNote, PaymentMethod,
|
||||
PaymentAccount, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 佣金结算单导出查询参数。
|
||||
/// 佣金结算单导出查询参数。
|
||||
/// </summary>
|
||||
public sealed class CommissionSettlementExportQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 结算单 ID。
|
||||
/// 结算单 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid SettlementId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 导出格式。
|
||||
/// 导出格式。
|
||||
/// </summary>
|
||||
[StringLength(10)]
|
||||
public string? Format { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 佣金结算单凭证查询参数。
|
||||
/// 佣金结算单凭证查询参数。
|
||||
/// </summary>
|
||||
public sealed class CommissionSettlementProofQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 结算单 ID。
|
||||
/// 结算单 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid SettlementId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建佣金结算凭证请求。
|
||||
/// 创建佣金结算凭证请求。
|
||||
/// </summary>
|
||||
public sealed class CreateCommissionProofDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 结算单 ID。
|
||||
/// 结算单 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid SettlementId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 凭证类型。
|
||||
/// 凭证类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? ProofType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[StringLength(2000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资产 ID。
|
||||
/// 资产 ID。
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 外部链接。
|
||||
/// 外部链接。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? ExternalUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 金额,单位为分。
|
||||
/// 金额,单位为分。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? AmountCents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付方式。
|
||||
/// 支付方式。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? PaymentMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收款账号。
|
||||
/// 收款账号。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? PaymentAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付时间。
|
||||
/// 支付时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
public CreateCommissionProofCommand ToCommand() => new(SettlementId, ProofType, Title, Description, AssetId, ExternalUrl, AmountCents, PaymentMethod, PaymentAccount, PaidAt, Metadata);
|
||||
|
||||
public CreateCommissionProofCommand ToCommand()
|
||||
{
|
||||
return new CreateCommissionProofCommand(SettlementId, ProofType, Title, Description, AssetId, ExternalUrl,
|
||||
AmountCents, PaymentMethod,
|
||||
PaymentAccount, PaidAt, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新佣金结算凭证状态请求。
|
||||
/// 更新佣金结算凭证状态请求。
|
||||
/// </summary>
|
||||
public sealed class UpdateCommissionProofStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 凭证 ID。
|
||||
/// 凭证 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ProofId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 审核备注。
|
||||
/// 审核备注。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? ReviewNote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
public UpdateCommissionProofStatusCommand ToCommand() => new(ProofId, Status, ReviewNote, Metadata);
|
||||
}
|
||||
|
||||
public UpdateCommissionProofStatusCommand ToCommand()
|
||||
{
|
||||
return new UpdateCommissionProofStatusCommand(ProofId, Status, ReviewNote, Metadata);
|
||||
}
|
||||
}
|
||||
@@ -8,74 +8,74 @@ using Tiku.Domain.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 内容管理查询参数。
|
||||
/// 内容管理查询参数。
|
||||
/// </summary>
|
||||
public sealed class ContentManagementQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
|
||||
public string? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入口类型。
|
||||
/// 入口类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? EntryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集类型。
|
||||
/// 题集类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? CollectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标记类型。
|
||||
/// 标记类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? MarkerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含停用数据。
|
||||
/// 是否包含停用数据。
|
||||
/// </summary>
|
||||
public bool IncludeInactive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 1000)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -99,86 +99,86 @@ public sealed class ContentManagementQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新内容条目请求 DTO。
|
||||
/// 新增或更新内容条目请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertContentEntryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 历史系统 ID。
|
||||
/// 历史系统 ID。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入口键。
|
||||
/// 入口键。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? EntryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 入口类型。
|
||||
/// 入口类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? EntryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图标。
|
||||
/// 图标。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 路由地址。
|
||||
/// 路由地址。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Route { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[StringLength(2000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可见性。
|
||||
/// 可见性。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问规则。
|
||||
/// 访问规则。
|
||||
/// </summary>
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 布局配置。
|
||||
/// 布局配置。
|
||||
/// </summary>
|
||||
public JsonElement LayoutConfig { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool? IsActive { get; set; }
|
||||
|
||||
@@ -203,94 +203,94 @@ public sealed class UpsertContentEntryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新内容节点请求 DTO。
|
||||
/// 新增或更新内容节点请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertContentNodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父节点 ID。
|
||||
/// 父节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 历史系统 ID。
|
||||
/// 历史系统 ID。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点键。
|
||||
/// 节点键。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? NodeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 节点Type。
|
||||
/// 节点Type。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? NodeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标记类型。
|
||||
/// 标记类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? MarkerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标记配置。
|
||||
/// 标记配置。
|
||||
/// </summary>
|
||||
public JsonElement MarkerConfig { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool? IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可选择。
|
||||
/// 是否可选择。
|
||||
/// </summary>
|
||||
public bool? IsSelectable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否叶子节点。
|
||||
/// 是否叶子节点。
|
||||
/// </summary>
|
||||
public bool? IsLeaf { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问规则。
|
||||
/// 访问规则。
|
||||
/// </summary>
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -317,103 +317,103 @@ public sealed class UpsertContentNodeDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新题目题集请求 DTO。
|
||||
/// 新增或更新题目题集请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertQuestionCollectionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题库 ID。
|
||||
/// 题库 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionBankId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 历史系统 ID。
|
||||
/// 历史系统 ID。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 题集类型。
|
||||
/// 题集类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? CollectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源类型。
|
||||
/// 来源类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? SourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 筛选条件。
|
||||
/// 筛选条件。
|
||||
/// </summary>
|
||||
public JsonElement Filters { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 总分。
|
||||
/// 总分。
|
||||
/// </summary>
|
||||
public decimal? TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时长,单位为分钟。
|
||||
/// 时长,单位为分钟。
|
||||
/// </summary>
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问规则。
|
||||
/// 访问规则。
|
||||
/// </summary>
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -442,45 +442,45 @@ public sealed class UpsertQuestionCollectionDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 题集题目请求 DTO。
|
||||
/// 题集题目请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CollectionQuestionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
|
||||
|
||||
/// <summary>
|
||||
/// 分段键。
|
||||
/// 分段键。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? SectionKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分数。
|
||||
/// 分数。
|
||||
/// </summary>
|
||||
public decimal? Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否必填。
|
||||
/// 是否必填。
|
||||
/// </summary>
|
||||
public bool? Required { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -497,18 +497,18 @@ public sealed class CollectionQuestionDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换题集Items请求 DTO。
|
||||
/// 替换题集Items请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ReplaceCollectionItemsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目列表。
|
||||
/// 题目列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<CollectionQuestionDto> Questions { get; set; } = [];
|
||||
|
||||
@@ -521,103 +521,103 @@ public sealed class ReplaceCollectionItemsDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新练习Blueprint请求 DTO。
|
||||
/// 新增或更新练习Blueprint请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertPracticeBlueprintDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 历史系统 ID。
|
||||
/// 历史系统 ID。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 组卷方式。
|
||||
/// 组卷方式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssemblyType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目数量上限。
|
||||
/// 题目数量上限。
|
||||
/// </summary>
|
||||
public int? QuestionLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时长,单位为分钟。
|
||||
/// 时长,单位为分钟。
|
||||
/// </summary>
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总分。
|
||||
/// 总分。
|
||||
/// </summary>
|
||||
public decimal? TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 及格分。
|
||||
/// 及格分。
|
||||
/// </summary>
|
||||
public decimal? PassScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分段配置。
|
||||
/// 分段配置。
|
||||
/// </summary>
|
||||
public JsonElement Sections { get; set; } = JsonDefaults.Array();
|
||||
|
||||
/// <summary>
|
||||
/// 规则配置。
|
||||
/// 规则配置。
|
||||
/// </summary>
|
||||
public JsonElement Rules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 访问规则。
|
||||
/// 访问规则。
|
||||
/// </summary>
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序。
|
||||
/// 显示顺序。
|
||||
/// </summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
@@ -646,20 +646,20 @@ public sealed class UpsertPracticeBlueprintDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入Template查询参数。
|
||||
/// 导入Template查询参数。
|
||||
/// </summary>
|
||||
public sealed class ImportTemplateQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入Type。
|
||||
/// 导入Type。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string ImportType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 导出格式。
|
||||
/// 导出格式。
|
||||
/// </summary>
|
||||
[StringLength(10)]
|
||||
public string? Format { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,85 +4,85 @@ using Tiku.Application.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 内容Navigation查询参数。
|
||||
/// 内容Navigation查询参数。
|
||||
/// </summary>
|
||||
public sealed class ContentNavigationQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// 父节点 ID;传 root 表示根节点。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
|
||||
public string? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 入口类型。
|
||||
/// 入口类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? EntryType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集类型。
|
||||
/// 题集类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? CollectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标记类型。
|
||||
/// 标记类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? MarkerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含隐藏数据。
|
||||
/// 是否包含隐藏数据。
|
||||
/// </summary>
|
||||
public bool IncludeHidden { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含停用数据。
|
||||
/// 是否包含停用数据。
|
||||
/// </summary>
|
||||
public bool IncludeInactive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 1000)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -113,4 +113,4 @@ public sealed class ContentNavigationQueryDto
|
||||
IncludeInactive,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,69 +5,69 @@ using Tiku.Application.Growth;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新CRM配置请求 DTO。
|
||||
/// 新增或更新CRM配置请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertCrmConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问地址。
|
||||
/// 访问地址。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥内容。
|
||||
/// 密钥内容。
|
||||
/// </summary>
|
||||
public string? Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表单名称。
|
||||
/// 表单名称。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? FormName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 考试类型。
|
||||
/// 考试类型。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? ExamType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时长,单位为秒。
|
||||
/// 超时时长,单位为秒。
|
||||
/// </summary>
|
||||
[Range(1, 120)]
|
||||
public int? TimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 延迟秒数。
|
||||
/// 延迟秒数。
|
||||
/// </summary>
|
||||
[Range(0, 86400)]
|
||||
public int? DelaySeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配模式。
|
||||
/// 分配模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssignmentMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配池。
|
||||
/// 分配池。
|
||||
/// </summary>
|
||||
public JsonElement? AssignmentPool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配配置。
|
||||
/// 分配配置。
|
||||
/// </summary>
|
||||
public JsonElement? AssignmentConfig { get; set; }
|
||||
|
||||
@@ -89,29 +89,29 @@ public sealed class UpsertCrmConfigDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRM队列查询参数。
|
||||
/// CRM队列查询参数。
|
||||
/// </summary>
|
||||
public sealed class CrmQueueQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 队列任务 ID。
|
||||
/// 队列任务 ID。
|
||||
/// </summary>
|
||||
public Guid? QueueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -123,17 +123,17 @@ public sealed class CrmQueueQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRM队列日志查询参数。
|
||||
/// CRM队列日志查询参数。
|
||||
/// </summary>
|
||||
public sealed class CrmQueueLogQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 队列任务 ID。
|
||||
/// 队列任务 ID。
|
||||
/// </summary>
|
||||
public Guid? QueueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -145,30 +145,30 @@ public sealed class CrmQueueLogQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CRM队列Action请求 DTO。
|
||||
/// CRM队列Action请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CrmQueueActionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 队列任务 ID。
|
||||
/// 队列任务 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid QueueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作。
|
||||
/// 操作。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Note { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
@@ -176,4 +176,4 @@ public sealed class CrmQueueActionDto
|
||||
{
|
||||
return new CrmQueueActionCommand(QueueId, Action, Note, Metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 健康检查响应。
|
||||
/// 健康检查响应。
|
||||
/// </summary>
|
||||
/// <param name="Status">服务状态,例如 ok。</param>
|
||||
/// <param name="Service">服务名称。</param>
|
||||
/// <param name="CheckedAt">检查时间。</param>
|
||||
/// <summary>
|
||||
/// 健康检查Response请求 DTO。
|
||||
/// 健康检查Response请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record HealthResponseDto(
|
||||
string Status,
|
||||
string Service,
|
||||
DateTimeOffset CheckedAt);
|
||||
DateTimeOffset CheckedAt);
|
||||
@@ -8,24 +8,24 @@ using Tiku.Domain.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 学习Limit查询参数。
|
||||
/// 学习Limit查询参数。
|
||||
/// </summary>
|
||||
public sealed class LearningLimitQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单元 ID。
|
||||
/// 单元 ID。
|
||||
/// </summary>
|
||||
public Guid? UnitId { get; set; }
|
||||
|
||||
@@ -36,34 +36,34 @@ public sealed class LearningLimitQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 练习会话查询参数。
|
||||
/// 练习会话查询参数。
|
||||
/// </summary>
|
||||
public sealed class PracticeSessionQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 练习会话 ID。
|
||||
/// 练习会话 ID。
|
||||
/// </summary>
|
||||
public Guid? PracticeSessionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 练习蓝图 ID。
|
||||
/// 练习蓝图 ID。
|
||||
/// </summary>
|
||||
public Guid? BlueprintId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -80,67 +80,67 @@ public sealed class PracticeSessionQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建练习会话请求 DTO。
|
||||
/// 创建练习会话请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreatePracticeSessionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标类型。
|
||||
/// 目标类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? TargetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标 ID。
|
||||
/// 目标 ID。
|
||||
/// </summary>
|
||||
public Guid? TargetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 练习蓝图 ID。
|
||||
/// 练习蓝图 ID。
|
||||
/// </summary>
|
||||
public Guid? BlueprintId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目数量上限。
|
||||
/// 题目数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? QuestionLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时长,单位为分钟。
|
||||
/// 时长,单位为分钟。
|
||||
/// </summary>
|
||||
[Range(1, 1440)]
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总分。
|
||||
/// 总分。
|
||||
/// </summary>
|
||||
[Range(typeof(decimal), "0", "99999")]
|
||||
public decimal? TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
@@ -162,62 +162,67 @@ public sealed class CreatePracticeSessionDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交练习会话请求 DTO。
|
||||
/// 提交练习会话请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class SubmitPracticeSessionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 练习会话 ID。
|
||||
/// 练习会话 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid PracticeSessionId { get; set; }
|
||||
|
||||
[Required, Range(1, long.MaxValue)]
|
||||
public long ExpectedSessionVersion { get; set; }
|
||||
[Required] [Range(1, long.MaxValue)] public long ExpectedSessionVersion { get; set; }
|
||||
|
||||
[Required, StringLength(200, MinimumLength = 1)]
|
||||
[Required]
|
||||
[StringLength(200, MinimumLength = 1)]
|
||||
public string IdempotencyKey { get; set; } = string.Empty;
|
||||
|
||||
public SubmitPracticeSessionCommand ToCommand() =>
|
||||
new(PracticeSessionId, ExpectedSessionVersion, IdempotencyKey);
|
||||
public SubmitPracticeSessionCommand ToCommand()
|
||||
{
|
||||
return new SubmitPracticeSessionCommand(PracticeSessionId, ExpectedSessionVersion, IdempotencyKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交答案请求 DTO。
|
||||
/// 提交答案请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class SubmitAnswerDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 会话题目 ID。
|
||||
/// 会话题目 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid SessionQuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户端读取会话时获得的版本。
|
||||
/// 客户端读取会话时获得的版本。
|
||||
/// </summary>
|
||||
[Required, Range(1, long.MaxValue)]
|
||||
[Required]
|
||||
[Range(1, long.MaxValue)]
|
||||
public long ExpectedSessionVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户端在本会话内单调递增的序列。
|
||||
/// 客户端在本会话内单调递增的序列。
|
||||
/// </summary>
|
||||
[Required, Range(1, long.MaxValue)]
|
||||
[Required]
|
||||
[Range(1, long.MaxValue)]
|
||||
public long ClientSequence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 网络重试幂等键。
|
||||
/// 网络重试幂等键。
|
||||
/// </summary>
|
||||
[Required, StringLength(200, MinimumLength = 1)]
|
||||
[Required]
|
||||
[StringLength(200, MinimumLength = 1)]
|
||||
public string IdempotencyKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 已选选项的零基索引。
|
||||
/// 已选选项的零基索引。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<int>? SelectedOptionIndices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文字答案。
|
||||
/// 文字答案。
|
||||
/// </summary>
|
||||
[StringLength(10000)]
|
||||
public string? AnswerText { get; set; }
|
||||
@@ -235,24 +240,24 @@ public sealed class SubmitAnswerDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 题目Action请求 DTO。
|
||||
/// 题目Action请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class QuestionActionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public QuestionSource Source { get; set; } = QuestionSource.Tenant;
|
||||
|
||||
/// <summary>
|
||||
/// 是否收藏。
|
||||
/// 是否收藏。
|
||||
/// </summary>
|
||||
public bool? Favorite { get; set; }
|
||||
|
||||
@@ -263,36 +268,36 @@ public sealed class QuestionActionDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单词Progress请求 DTO。
|
||||
/// 单词Progress请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class WordProgressDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 单词 ID。
|
||||
/// 单词 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid WordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 答对数量变化。
|
||||
/// 答对数量变化。
|
||||
/// </summary>
|
||||
[Range(0, 100)]
|
||||
public int? CorrectDelta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 答错数量变化。
|
||||
/// 答错数量变化。
|
||||
/// </summary>
|
||||
[Range(0, 100)]
|
||||
public int? WrongDelta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下次复习时间。
|
||||
/// 下次复习时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? NextReviewAt { get; set; }
|
||||
|
||||
@@ -308,23 +313,23 @@ public sealed class WordProgressDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收藏单词请求 DTO。
|
||||
/// 收藏单词请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class FavoriteWordDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 单词 ID。
|
||||
/// 单词 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid WordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否收藏。
|
||||
/// 是否收藏。
|
||||
/// </summary>
|
||||
public bool? Favorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Note { get; set; }
|
||||
@@ -336,24 +341,24 @@ public sealed class FavoriteWordDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单词Review请求 DTO。
|
||||
/// 单词Review请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class WordReviewDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 单词 ID。
|
||||
/// 单词 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid WordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结果。
|
||||
/// 结果。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下次复习时间。
|
||||
/// 下次复习时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? NextReviewAt { get; set; }
|
||||
|
||||
@@ -361,4 +366,4 @@ public sealed class WordReviewDto
|
||||
{
|
||||
return new WordReviewCommand(WordId, Result, NextReviewAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -10,89 +9,98 @@ using Tiku.Domain.Tenancy;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 平台管理端查询参数。
|
||||
/// 平台管理端查询参数。
|
||||
/// </summary>
|
||||
public sealed class PlatformAdminQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 搜索关键字。
|
||||
/// 搜索关键字。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Search { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public PlatformAdminQuery ToQuery() => new(Status, Search, Limit);
|
||||
public PlatformAdminQuery ToQuery()
|
||||
{
|
||||
return new PlatformAdminQuery(Status, Search, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建平台租户请求 DTO。
|
||||
/// 创建平台租户请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreatePlatformTenantDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 短编码。
|
||||
/// 短编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 法定名称。
|
||||
/// 法定名称。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? LegalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public TenantStatus Status { get; set; } = TenantStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 账务状态。
|
||||
/// 账务状态。
|
||||
/// </summary>
|
||||
public BillingStatus BillingStatus { get; set; } = BillingStatus.Trial;
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>租户首次开通使用的自定义主域名。</summary>
|
||||
[Required, StringLength(253, MinimumLength = 4)]
|
||||
[Required]
|
||||
[StringLength(253, MinimumLength = 4)]
|
||||
public string PrimaryDomainHost { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 租户负责人邮箱。
|
||||
/// 租户负责人邮箱。
|
||||
/// </summary>
|
||||
[EmailAddress, StringLength(320)]
|
||||
[EmailAddress]
|
||||
[StringLength(320)]
|
||||
public string? OwnerEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户负责人手机号。
|
||||
/// 租户负责人手机号。
|
||||
/// </summary>
|
||||
[Phone, StringLength(32)]
|
||||
[Phone]
|
||||
[StringLength(32)]
|
||||
public string? OwnerPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户负责人姓名。
|
||||
/// 租户负责人姓名。
|
||||
/// </summary>
|
||||
[Required, StringLength(100)]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string OwnerName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>初始试用套餐版本;为空时使用平台配置的默认基础套餐。</summary>
|
||||
@@ -106,7 +114,8 @@ public sealed class CreatePlatformTenantDto
|
||||
public TenantBillingCollectionMode CollectionMode { get; set; } = TenantBillingCollectionMode.Online;
|
||||
|
||||
/// <summary>默认支付 Provider。</summary>
|
||||
[Required, StringLength(50)]
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string DefaultPaymentProvider { get; set; } = "manual";
|
||||
|
||||
/// <summary>是否自动生成续费应收。</summary>
|
||||
@@ -116,408 +125,456 @@ public sealed class CreatePlatformTenantDto
|
||||
[Range(1, 90)]
|
||||
public int RenewalLeadDays { get; set; } = 14;
|
||||
|
||||
public CreatePlatformTenantCommand ToCommand(string idempotencyKey) => new(
|
||||
Slug, Name, LegalName, Status, BillingStatus, Metadata, PrimaryDomainHost,
|
||||
OwnerEmail, OwnerPhone, OwnerName,
|
||||
InitialOfferingVersionId, TrialDays, CollectionMode, DefaultPaymentProvider,
|
||||
AutoGenerateRenewal, RenewalLeadDays, idempotencyKey);
|
||||
public CreatePlatformTenantCommand ToCommand(string idempotencyKey)
|
||||
{
|
||||
return new CreatePlatformTenantCommand(
|
||||
Slug, Name, LegalName, Status, BillingStatus, Metadata, PrimaryDomainHost,
|
||||
OwnerEmail, OwnerPhone, OwnerName,
|
||||
InitialOfferingVersionId, TrialDays, CollectionMode, DefaultPaymentProvider,
|
||||
AutoGenerateRenewal, RenewalLeadDays, idempotencyKey);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReplacePlatformPrimaryDomainDto
|
||||
{
|
||||
[Required, StringLength(253, MinimumLength = 4)]
|
||||
[Required]
|
||||
[StringLength(253, MinimumLength = 4)]
|
||||
public string Host { get; set; } = string.Empty;
|
||||
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
public ReplacePlatformPrimaryDomainCommand ToCommand(Guid tenantId) => new(tenantId, Host, Reason);
|
||||
public ReplacePlatformPrimaryDomainCommand ToCommand(Guid tenantId)
|
||||
{
|
||||
return new ReplacePlatformPrimaryDomainCommand(tenantId, Host, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class IssuePlatformOwnerActivationLinkDto
|
||||
{
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
public bool ReplaceExisting { get; set; }
|
||||
|
||||
public IssuePlatformOwnerActivationLinkCommand ToCommand(Guid tenantId, string idempotencyKey) =>
|
||||
new(tenantId, idempotencyKey, Reason, ReplaceExisting);
|
||||
public IssuePlatformOwnerActivationLinkCommand ToCommand(Guid tenantId, string idempotencyKey)
|
||||
{
|
||||
return new IssuePlatformOwnerActivationLinkCommand(tenantId, idempotencyKey, Reason, ReplaceExisting);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>更新租户收款策略。</summary>
|
||||
public sealed class UpsertTenantBillingPolicyDto
|
||||
{
|
||||
public TenantBillingCollectionMode CollectionMode { get; set; } = TenantBillingCollectionMode.Online;
|
||||
[Required, StringLength(50)]
|
||||
public string DefaultPaymentProvider { get; set; } = "manual";
|
||||
|
||||
[Required] [StringLength(50)] public string DefaultPaymentProvider { get; set; } = "manual";
|
||||
|
||||
public bool AutoGenerateRenewal { get; set; } = true;
|
||||
[Range(1, 90)]
|
||||
public int RenewalLeadDays { get; set; } = 14;
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
|
||||
[Range(1, 90)] public int RenewalLeadDays { get; set; } = 14;
|
||||
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
public UpsertTenantBillingPolicyCommand ToCommand(Guid tenantId) => new(
|
||||
tenantId,
|
||||
CollectionMode,
|
||||
DefaultPaymentProvider,
|
||||
AutoGenerateRenewal,
|
||||
RenewalLeadDays,
|
||||
Reason);
|
||||
public UpsertTenantBillingPolicyCommand ToCommand(Guid tenantId)
|
||||
{
|
||||
return new UpsertTenantBillingPolicyCommand(
|
||||
tenantId,
|
||||
CollectionMode,
|
||||
DefaultPaymentProvider,
|
||||
AutoGenerateRenewal,
|
||||
RenewalLeadDays,
|
||||
Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台租户状态请求 DTO。
|
||||
/// 更新平台租户状态请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpdatePlatformTenantStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public TenantStatus Status { get; set; } = TenantStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 原因。
|
||||
/// 原因。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public UpdatePlatformTenantStatusCommand ToCommand() => new(TenantId, Status, Reason);
|
||||
public UpdatePlatformTenantStatusCommand ToCommand()
|
||||
{
|
||||
return new UpdatePlatformTenantStatusCommand(TenantId, Status, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新平台租户账务资料请求 DTO。
|
||||
/// 新增或更新平台租户账务资料请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformTenantBillingProfileDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账务名称。
|
||||
/// 账务名称。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? BillingName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 税号。
|
||||
/// 税号。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TaxId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系人姓名。
|
||||
/// 联系人姓名。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? ContactName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系人手机号。
|
||||
/// 联系人手机号。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系人邮箱。
|
||||
/// 联系人邮箱。
|
||||
/// </summary>
|
||||
[StringLength(320)]
|
||||
public string? ContactEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账务地址。
|
||||
/// 账务地址。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? BillingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票抬头。
|
||||
/// 发票抬头。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? InvoiceTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发票类型。
|
||||
/// 发票类型。
|
||||
/// </summary>
|
||||
public TenantBillingInvoiceTitleType? InvoiceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开户银行。
|
||||
/// 开户银行。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? BankName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 脱敏银行账号。
|
||||
/// 脱敏银行账号。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? BankAccountMasked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformTenantBillingProfileCommand ToCommand() => new(
|
||||
TenantId,
|
||||
BillingName,
|
||||
TaxId,
|
||||
ContactName,
|
||||
ContactPhone,
|
||||
ContactEmail,
|
||||
BillingAddress,
|
||||
InvoiceTitle,
|
||||
InvoiceType,
|
||||
BankName,
|
||||
BankAccountMasked,
|
||||
Metadata);
|
||||
public UpsertPlatformTenantBillingProfileCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformTenantBillingProfileCommand(
|
||||
TenantId,
|
||||
BillingName,
|
||||
TaxId,
|
||||
ContactName,
|
||||
ContactPhone,
|
||||
ContactEmail,
|
||||
BillingAddress,
|
||||
InvoiceTitle,
|
||||
InvoiceType,
|
||||
BankName,
|
||||
BankAccountMasked,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新平台Staff请求 DTO。
|
||||
/// 新增或更新平台Staff请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformStaffDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱。
|
||||
/// 邮箱。
|
||||
/// </summary>
|
||||
[StringLength(320)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 手机号。
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID 列表。
|
||||
/// 角色 ID 列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Guid> RoleIds { get; set; } = [];
|
||||
|
||||
public UpsertPlatformStaffCommand ToCommand() => new(UserId, Email, Phone, Name, Status, RoleIds);
|
||||
public UpsertPlatformStaffCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformStaffCommand(UserId, Email, Phone, Name, Status, RoleIds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台Staff状态请求 DTO。
|
||||
/// 更新平台Staff状态请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpdatePlatformStaffStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public UserStatus Status { get; set; } = UserStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 原因。
|
||||
/// 原因。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public UpdatePlatformStaffStatusCommand ToCommand() => new(UserId, Status, Reason);
|
||||
public UpdatePlatformStaffStatusCommand ToCommand()
|
||||
{
|
||||
return new UpdatePlatformStaffStatusCommand(UserId, Status, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新平台审计Alert状态请求 DTO。
|
||||
/// 更新平台审计Alert状态请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpdatePlatformAuditAlertStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 告警 ID。
|
||||
/// 告警 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid AlertId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public PlatformAuditAlertStatus Status { get; set; } = PlatformAuditAlertStatus.Acknowledged;
|
||||
|
||||
/// <summary>
|
||||
/// 处理备注。
|
||||
/// 处理备注。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? ResolutionNote { get; set; }
|
||||
|
||||
public UpdatePlatformAuditAlertStatusCommand ToCommand() => new(AlertId, Status, ResolutionNote);
|
||||
public UpdatePlatformAuditAlertStatusCommand ToCommand()
|
||||
{
|
||||
return new UpdatePlatformAuditAlertStatusCommand(AlertId, Status, ResolutionNote);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新平台DunningChannel请求 DTO。
|
||||
/// 新增或更新平台DunningChannel请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformBillingDunningChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知渠道 ID。
|
||||
/// 通知渠道 ID。
|
||||
/// </summary>
|
||||
public Guid? ChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道编码。
|
||||
/// 渠道编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string ChannelCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
public PlatformBillingDunningProvider Provider { get; set; } = PlatformBillingDunningProvider.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Webhook 地址。
|
||||
/// Webhook 地址。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(2048)]
|
||||
public string WebhookUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提醒类型列表。
|
||||
/// 提醒类型列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> ReminderTypes { get; set; } = ["overdue", "final_notice"];
|
||||
|
||||
/// <summary>
|
||||
/// 提醒渠道列表。
|
||||
/// 提醒渠道列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> ReminderChannels { get; set; } = ["internal"];
|
||||
|
||||
/// <summary>
|
||||
/// 最低提醒级别。
|
||||
/// 最低提醒级别。
|
||||
/// </summary>
|
||||
[Range(1, 20)]
|
||||
public int MinReminderLevel { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID 列表。
|
||||
/// 租户 ID 列表。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Guid> TenantIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 超时时长,单位为秒。
|
||||
/// 超时时长,单位为秒。
|
||||
/// </summary>
|
||||
[Range(1, 60)]
|
||||
public int TimeoutSeconds { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformBillingDunningChannelCommand ToCommand() => new(
|
||||
ChannelId,
|
||||
ChannelCode,
|
||||
Name,
|
||||
Description,
|
||||
Enabled,
|
||||
Provider,
|
||||
WebhookUrl,
|
||||
SecretRef,
|
||||
ReminderTypes,
|
||||
ReminderChannels,
|
||||
MinReminderLevel,
|
||||
TenantIds,
|
||||
TimeoutSeconds,
|
||||
Metadata);
|
||||
public UpsertPlatformBillingDunningChannelCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformBillingDunningChannelCommand(
|
||||
ChannelId,
|
||||
ChannelCode,
|
||||
Name,
|
||||
Description,
|
||||
Enabled,
|
||||
Provider,
|
||||
WebhookUrl,
|
||||
SecretRef,
|
||||
ReminderTypes,
|
||||
ReminderChannels,
|
||||
MinReminderLevel,
|
||||
TenantIds,
|
||||
TimeoutSeconds,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用平台DunningChannel请求 DTO。
|
||||
/// 禁用平台DunningChannel请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class DisablePlatformBillingDunningChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知渠道 ID。
|
||||
/// 通知渠道 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原因。
|
||||
/// 原因。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public DisablePlatformBillingDunningChannelCommand ToCommand() => new(ChannelId, Reason);
|
||||
public DisablePlatformBillingDunningChannelCommand ToCommand()
|
||||
{
|
||||
return new DisablePlatformBillingDunningChannelCommand(ChannelId, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重试平台Dunning事件请求 DTO。
|
||||
/// 重试平台Dunning事件请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class RetryPlatformBillingDunningEventDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 事件 ID。
|
||||
/// 事件 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid EventId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原因。
|
||||
/// 原因。
|
||||
/// </summary>
|
||||
[StringLength(1000)]
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public RetryPlatformBillingDunningEventCommand ToCommand() => new(EventId, Reason);
|
||||
public RetryPlatformBillingDunningEventCommand ToCommand()
|
||||
{
|
||||
return new RetryPlatformBillingDunningEventCommand(EventId, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>人工确认或忽略催缴投递事件。</summary>
|
||||
public sealed class ResolvePlatformBillingDunningEventDto
|
||||
{
|
||||
[Required]
|
||||
public Guid EventId { get; set; }
|
||||
[Required] public Guid EventId { get; set; }
|
||||
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
public ResolvePlatformBillingDunningEventCommand ToCommand() => new(EventId, Reason);
|
||||
}
|
||||
public ResolvePlatformBillingDunningEventCommand ToCommand()
|
||||
{
|
||||
return new ResolvePlatformBillingDunningEventCommand(EventId, Reason);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
public sealed record PlatformApprovalDecisionDto([Required, MaxLength(1000)] string Reason);
|
||||
public sealed record PlatformApprovalDecisionDto([Required] [MaxLength(1000)] string Reason);
|
||||
|
||||
public sealed record UpdatePlatformApprovalPolicyDto(
|
||||
bool Enabled,
|
||||
@@ -14,11 +14,14 @@ public sealed record UpdatePlatformApprovalPolicyDto(
|
||||
[Range(1, 720)] int ExpiresAfterHours,
|
||||
JsonElement? Conditions)
|
||||
{
|
||||
public UpdatePlatformApprovalPolicyCommand ToCommand(string code) => new(
|
||||
code,
|
||||
Enabled,
|
||||
AlwaysRequireApproval,
|
||||
AmountThresholdCents,
|
||||
ExpiresAfterHours,
|
||||
Conditions?.Clone() ?? JsonDefaults.Object());
|
||||
}
|
||||
public UpdatePlatformApprovalPolicyCommand ToCommand(string code)
|
||||
{
|
||||
return new UpdatePlatformApprovalPolicyCommand(
|
||||
code,
|
||||
Enabled,
|
||||
AlwaysRequireApproval,
|
||||
AmountThresholdCents,
|
||||
ExpiresAfterHours,
|
||||
Conditions?.Clone() ?? JsonDefaults.Object());
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,47 @@ using Tiku.Domain.Platform;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
public sealed record SavePlatformConfigurationDraftDto(
|
||||
[Required, MaxLength(120)] string DefinitionCode,
|
||||
[Required, MaxLength(80)] string Environment,
|
||||
[Required] [MaxLength(120)] string DefinitionCode,
|
||||
[Required] [MaxLength(80)] string Environment,
|
||||
JsonElement? Value,
|
||||
[MaxLength(300)] string? SecretRef,
|
||||
[Required, MaxLength(1000)] string Reason)
|
||||
[Required] [MaxLength(1000)] string Reason)
|
||||
{
|
||||
public SavePlatformConfigurationDraftCommand ToCommand() => new(DefinitionCode, Environment, Value, SecretRef, Reason);
|
||||
public SavePlatformConfigurationDraftCommand ToCommand()
|
||||
{
|
||||
return new SavePlatformConfigurationDraftCommand(DefinitionCode, Environment, Value, SecretRef, Reason);
|
||||
}
|
||||
}
|
||||
public sealed record PlatformRollbackDto([Required, MaxLength(1000)] string Reason);
|
||||
public sealed record UpsertPlatformNotificationTemplateDto(Guid? Id, [Required, MaxLength(120)] string Code,
|
||||
[Required, MaxLength(200)] string Name, PlatformNotificationChannel Channel,
|
||||
[Required, MaxLength(500)] string SubjectTemplate, [Required, MaxLength(8000)] string BodyTemplate,
|
||||
bool Enabled, JsonElement? Variables)
|
||||
|
||||
public sealed record PlatformRollbackDto([Required] [MaxLength(1000)] string Reason);
|
||||
|
||||
public sealed record UpsertPlatformNotificationTemplateDto(
|
||||
Guid? Id,
|
||||
[Required] [MaxLength(120)] string Code,
|
||||
[Required] [MaxLength(200)] string Name,
|
||||
PlatformNotificationChannel Channel,
|
||||
[Required] [MaxLength(500)] string SubjectTemplate,
|
||||
[Required] [MaxLength(8000)] string BodyTemplate,
|
||||
bool Enabled,
|
||||
JsonElement? Variables)
|
||||
{
|
||||
public UpsertPlatformNotificationTemplateCommand ToCommand() => new(Id, Code, Name, Channel, SubjectTemplate, BodyTemplate, Enabled, Variables?.Clone() ?? JsonDefaults.Array());
|
||||
public UpsertPlatformNotificationTemplateCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformNotificationTemplateCommand(Id, Code, Name, Channel, SubjectTemplate, BodyTemplate,
|
||||
Enabled,
|
||||
Variables?.Clone() ?? JsonDefaults.Array());
|
||||
}
|
||||
}
|
||||
public sealed record SendPlatformNotificationDto(Guid TemplateId, [MinLength(1)] IReadOnlyCollection<string> RoleCodes,
|
||||
|
||||
public sealed record SendPlatformNotificationDto(
|
||||
Guid TemplateId,
|
||||
[MinLength(1)] IReadOnlyCollection<string> RoleCodes,
|
||||
IReadOnlyDictionary<string, string>? Variables,
|
||||
[Required, MaxLength(200)] string IdempotencyKey)
|
||||
[Required] [MaxLength(200)] string IdempotencyKey)
|
||||
{
|
||||
public SendPlatformNotificationCommand ToCommand() => new(TemplateId, RoleCodes, Variables ?? new Dictionary<string, string>(), IdempotencyKey);
|
||||
}
|
||||
public SendPlatformNotificationCommand ToCommand()
|
||||
{
|
||||
return new SendPlatformNotificationCommand(TemplateId, RoleCodes, Variables ?? new Dictionary<string, string>(),
|
||||
IdempotencyKey);
|
||||
}
|
||||
}
|
||||
@@ -9,398 +9,497 @@ using Tiku.Domain.Tenancy;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 平台租户能力通用查询参数。
|
||||
/// 平台租户能力通用查询参数。
|
||||
/// </summary>
|
||||
public class PlatformCapabilityQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int Limit { get; set; } = 100;
|
||||
|
||||
public PlatformCapabilityQuery ToQuery() => new(TenantId, Status, Limit);
|
||||
public PlatformCapabilityQuery ToQuery()
|
||||
{
|
||||
return new PlatformCapabilityQuery(TenantId, Status, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新租户 CRM 配置请求。
|
||||
/// 新增或更新租户 CRM 配置请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformCrmConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回调地址。
|
||||
/// 回调地址。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 表单名称。
|
||||
/// 表单名称。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? FormName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 考试类型。
|
||||
/// 考试类型。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? ExamType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时长,单位为秒。
|
||||
/// 超时时长,单位为秒。
|
||||
/// </summary>
|
||||
[Range(1, 120)]
|
||||
public int? TimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 延迟秒数。
|
||||
/// 延迟秒数。
|
||||
/// </summary>
|
||||
[Range(0, 86400)]
|
||||
public int? DelaySeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配模式。
|
||||
/// 分配模式。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? AssignmentMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配池。
|
||||
/// 分配池。
|
||||
/// </summary>
|
||||
public JsonElement? AssignmentPool { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分配配置。
|
||||
/// 分配配置。
|
||||
/// </summary>
|
||||
public JsonElement? AssignmentConfig { get; set; }
|
||||
|
||||
public UpsertPlatformCrmConfigCommand ToCommand() =>
|
||||
new(Id, TenantId, Enabled, Url, SecretRef, FormName, ExamType, TimeoutSeconds, DelaySeconds, AssignmentMode, AssignmentPool, AssignmentConfig);
|
||||
public UpsertPlatformCrmConfigCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformCrmConfigCommand(Id, TenantId, Enabled, Url, SecretRef, FormName, ExamType,
|
||||
TimeoutSeconds, DelaySeconds,
|
||||
AssignmentMode, AssignmentPool, AssignmentConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重试租户 CRM 线索推送请求。
|
||||
/// 重试租户 CRM 线索推送请求。
|
||||
/// </summary>
|
||||
public sealed class RetryPlatformCrmLeadDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 队列任务 ID。
|
||||
/// 队列任务 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid QueueId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Note { get; set; }
|
||||
|
||||
public PlatformCrmLeadRetryCommand ToCommand() => new(QueueId, Note);
|
||||
public PlatformCrmLeadRetryCommand ToCommand()
|
||||
{
|
||||
return new PlatformCrmLeadRetryCommand(QueueId, Note);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台租户 CRM 日志查询参数。
|
||||
/// 平台租户 CRM 日志查询参数。
|
||||
/// </summary>
|
||||
public sealed class PlatformCrmLogQueryDto : PlatformCapabilityQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 队列任务 ID。
|
||||
/// 队列任务 ID。
|
||||
/// </summary>
|
||||
public Guid? QueueId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新租户短信渠道请求。
|
||||
/// 新增或更新租户短信渠道请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformSmsChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[Required, StringLength(80)]
|
||||
[Required]
|
||||
[StringLength(80)]
|
||||
public string Provider { get; set; } = "generic";
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required, StringLength(200)]
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 短信签名。
|
||||
/// 短信签名。
|
||||
/// </summary>
|
||||
[Required, StringLength(100)]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Signature { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 使用场景。
|
||||
/// 使用场景。
|
||||
/// </summary>
|
||||
[Required, StringLength(100)]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Scene { get; set; } = "login";
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public TenantExternalProviderStatus Status { get; set; } = TenantExternalProviderStatus.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
public int? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 月度配额。
|
||||
/// 月度配额。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? MonthlyQuota { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公开配置内容。
|
||||
/// 公开配置内容。
|
||||
/// </summary>
|
||||
public JsonElement ConfigPublic { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformSmsChannelCommand ToCommand() =>
|
||||
new(Id, TenantId, Provider, Name, Signature, Scene, Status, SecretRef, Priority, MonthlyQuota, ConfigPublic, Metadata);
|
||||
public UpsertPlatformSmsChannelCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformSmsChannelCommand(Id, TenantId, Provider, Name, Signature, Scene, Status, SecretRef,
|
||||
Priority, MonthlyQuota,
|
||||
ConfigPublic, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新租户短信模板请求。
|
||||
/// 新增或更新租户短信模板请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformSmsTemplateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 短信渠道 ID。
|
||||
/// 短信渠道 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ChannelId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
[Required, StringLength(120)]
|
||||
[Required]
|
||||
[StringLength(120)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required, StringLength(200)]
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
/// 类型。
|
||||
/// </summary>
|
||||
public SmsTemplateType Type { get; set; } = SmsTemplateType.Notification;
|
||||
|
||||
/// <summary>
|
||||
/// 审核状态。
|
||||
/// 审核状态。
|
||||
/// </summary>
|
||||
public SmsTemplateAuditStatus AuditStatus { get; set; } = SmsTemplateAuditStatus.Draft;
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public SmsTemplateStatus Status { get; set; } = SmsTemplateStatus.Active;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道模板编码。
|
||||
/// 渠道模板编码。
|
||||
/// </summary>
|
||||
[StringLength(120)]
|
||||
public string? ProviderTemplateCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模板内容。
|
||||
/// 模板内容。
|
||||
/// </summary>
|
||||
[Required, StringLength(1000)]
|
||||
[Required]
|
||||
[StringLength(1000)]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Remark。
|
||||
/// Remark。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformSmsTemplateCommand ToCommand() =>
|
||||
new(Id, TenantId, ChannelId, Code, Name, Type, AuditStatus, Status, ProviderTemplateCode, Content, Remark, Metadata);
|
||||
public UpsertPlatformSmsTemplateCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformSmsTemplateCommand(Id, TenantId, ChannelId, Code, Name, Type, AuditStatus, Status,
|
||||
ProviderTemplateCode, Content,
|
||||
Remark, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新平台支付应用请求。
|
||||
/// 新增或更新平台支付应用请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformPaymentAppDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付应用编码。
|
||||
/// 支付应用编码。
|
||||
/// </summary>
|
||||
[Required, StringLength(100)]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string AppCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 支付应用名称。
|
||||
/// 支付应用名称。
|
||||
/// </summary>
|
||||
[Required, StringLength(200)]
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string AppName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public PlatformPaymentAppStatus Status { get; set; } = PlatformPaymentAppStatus.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// 结算模式。
|
||||
/// 结算模式。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string SettlementMode { get; set; } = "PlatformCollect";
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformPaymentAppCommand ToCommand() =>
|
||||
new(Id, AppCode, AppName, Status, SettlementMode, Description, Metadata);
|
||||
public UpsertPlatformPaymentAppCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformPaymentAppCommand(Id, AppCode, AppName, Status, SettlementMode, Description, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新平台支付渠道请求。
|
||||
/// 新增或更新平台支付渠道请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformPaymentChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// ID。
|
||||
/// ID。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付应用 ID。
|
||||
/// 支付应用 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[Required, StringLength(80)]
|
||||
[Required]
|
||||
[StringLength(80)]
|
||||
public string Provider { get; set; } = "manual";
|
||||
|
||||
/// <summary>
|
||||
/// 模式。
|
||||
/// 模式。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string Mode { get; set; } = "PlatformCollect";
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public PlatformPaymentChannelStatus Status { get; set; } = PlatformPaymentChannelStatus.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// 显示名称。
|
||||
/// 显示名称。
|
||||
/// </summary>
|
||||
[Required, StringLength(200)]
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回调路径。
|
||||
/// 回调路径。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? CallbackPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
public int? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公开配置内容。
|
||||
/// 公开配置内容。
|
||||
/// </summary>
|
||||
public JsonElement ConfigPublic { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertPlatformPaymentChannelCommand ToCommand() =>
|
||||
new(Id, AppId, Provider, Mode, Status, DisplayName, SecretRef, CallbackPath, Priority, ConfigPublic, Metadata);
|
||||
public UpsertPlatformPaymentChannelCommand ToCommand()
|
||||
{
|
||||
return new UpsertPlatformPaymentChannelCommand(Id, AppId, Provider, Mode, Status, DisplayName, SecretRef,
|
||||
CallbackPath, Priority, ConfigPublic,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新租户支付应用请求。
|
||||
/// 新增或更新租户支付应用请求。
|
||||
/// </summary>
|
||||
public sealed class UpsertPlatformTenantPaymentAppDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[Required, StringLength(80)]
|
||||
[Required]
|
||||
[StringLength(80)]
|
||||
public string Provider { get; set; } = "manual";
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public TenantExternalProviderStatus Status { get; set; } = TenantExternalProviderStatus.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// 显示名称。
|
||||
/// 显示名称。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥引用。
|
||||
/// 密钥引用。
|
||||
/// </summary>
|
||||
[StringLength(300)]
|
||||
public string? SecretRef { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
public int? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公开配置内容。
|
||||
/// 公开配置内容。
|
||||
/// </summary>
|
||||
public JsonElement ConfigPublic { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertTenantExternalProviderCommand ToCommand() =>
|
||||
new(TenantExternalProviderCapability.Payment, Provider, Status, DisplayName, SecretRef, Priority, ConfigPublic, Metadata);
|
||||
}
|
||||
public UpsertTenantExternalProviderCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantExternalProviderCommand(TenantExternalProviderCapability.Payment, Provider, Status,
|
||||
DisplayName, SecretRef, Priority,
|
||||
ConfigPublic, Metadata);
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,24 @@ using Tiku.Application.Points;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 积分查询参数。
|
||||
/// 积分查询参数。
|
||||
/// </summary>
|
||||
public sealed class PointQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
@@ -32,25 +32,25 @@ public sealed class PointQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取积分任务请求 DTO。
|
||||
/// 领取积分任务请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ClaimPointTaskDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务键。
|
||||
/// 任务键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string TaskKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 来源类型。
|
||||
/// 来源类型。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? SourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源 ID。
|
||||
/// 来源 ID。
|
||||
/// </summary>
|
||||
public Guid? SourceId { get; set; }
|
||||
|
||||
@@ -61,12 +61,12 @@ public sealed class ClaimPointTaskDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建积分兑换订单请求 DTO。
|
||||
/// 创建积分兑换订单请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreatePointExchangeOrderDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 兑换项 ID。
|
||||
/// 兑换项 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ItemId { get; set; }
|
||||
@@ -75,4 +75,4 @@ public sealed class CreatePointExchangeOrderDto
|
||||
{
|
||||
return new CreatePointExchangeOrderCommand(ItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,105 +5,111 @@ using Tiku.Application.Profile;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 资料查询参数。
|
||||
/// 资料查询参数。
|
||||
/// </summary>
|
||||
public sealed class ProfileQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 最近记录数量上限。
|
||||
/// 最近记录数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 50)]
|
||||
public int? RecentLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 20)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
/// 类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含锁定资源。
|
||||
/// 是否包含锁定资源。
|
||||
/// </summary>
|
||||
public bool IncludeLocked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源类型。
|
||||
/// 来源类型。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? SourceType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间。
|
||||
/// 开始时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间。
|
||||
/// 结束时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset? To { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新资料请求 DTO。
|
||||
/// 更新资料请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpdateProfileDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 头像预设。
|
||||
/// 头像预设。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? AvatarPreset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已选择院校 ID。
|
||||
/// 已选择院校 ID。
|
||||
/// </summary>
|
||||
public Guid? SelectedSchoolId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已选择专业 ID。
|
||||
/// 已选择专业 ID。
|
||||
/// </summary>
|
||||
public Guid? SelectedMajorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 统计数据。
|
||||
/// 统计数据。
|
||||
/// </summary>
|
||||
public JsonElement? Stats { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 进度数据。
|
||||
/// 进度数据。
|
||||
/// </summary>
|
||||
public JsonElement? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模块选择数据。
|
||||
/// 模块选择数据。
|
||||
/// </summary>
|
||||
public JsonElement? ModuleSelections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近活动数据。
|
||||
/// 最近活动数据。
|
||||
/// </summary>
|
||||
public JsonElement? RecentActivities { get; set; }
|
||||
|
||||
@@ -123,12 +129,12 @@ public sealed class UpdateProfileDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知状态请求 DTO。
|
||||
/// 通知状态请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class NotificationStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知 ID 列表。
|
||||
/// 通知 ID 列表。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
@@ -136,7 +142,7 @@ public sealed class NotificationStatusDto
|
||||
public IReadOnlyCollection<Guid> NotificationIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Status { get; set; }
|
||||
@@ -148,58 +154,59 @@ public sealed class NotificationStatusDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交反馈请求 DTO。
|
||||
/// 提交反馈请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class SubmitFeedbackDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
/// 类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类。
|
||||
/// 分类。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 说明。
|
||||
/// 说明。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(5000)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 联系方式。
|
||||
/// 联系方式。
|
||||
/// </summary>
|
||||
[StringLength(200)]
|
||||
public string? Contact { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 附件列表。
|
||||
/// 附件列表。
|
||||
/// </summary>
|
||||
public JsonElement Attachments { get; set; } = JsonSerializer.SerializeToElement(Array.Empty<object>());
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonSerializer.SerializeToElement(new { });
|
||||
|
||||
@@ -216,4 +223,4 @@ public sealed class SubmitFeedbackDto
|
||||
Attachments,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,80 +5,80 @@ using Tiku.Domain.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 题目题库查询参数。
|
||||
/// 题目题库查询参数。
|
||||
/// </summary>
|
||||
public sealed class QuestionBankQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题库 ID。
|
||||
/// 题库 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionBankId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分类 ID。
|
||||
/// 分类 ID。
|
||||
/// </summary>
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点 ID。
|
||||
/// 节点 ID。
|
||||
/// </summary>
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题集 ID。
|
||||
/// 题集 ID。
|
||||
/// </summary>
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
public QuestionSource? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型。
|
||||
/// 类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目 ID 列表。
|
||||
/// 题目 ID 列表。
|
||||
/// </summary>
|
||||
public string? QuestionIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -105,10 +105,7 @@ public sealed class QuestionBankQueryDto
|
||||
|
||||
private Guid[] ParseQuestionIds()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(QuestionIds))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(QuestionIds)) return [];
|
||||
|
||||
return QuestionIds
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
@@ -119,4 +116,4 @@ public sealed class QuestionBankQueryDto
|
||||
.Take(300)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,30 +6,30 @@ using Tiku.Application.Growth;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 推荐租户查询参数。
|
||||
/// 推荐租户查询参数。
|
||||
/// </summary>
|
||||
public sealed class ReferralTenantQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐邀请码请求 DTO。
|
||||
/// 推荐邀请码请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ReferralInviteDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 渠道。
|
||||
/// 渠道。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Channel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 落地页路径。
|
||||
/// 落地页路径。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? LandingPath { get; set; }
|
||||
@@ -41,18 +41,18 @@ public sealed class ReferralInviteDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析推荐请求 DTO。
|
||||
/// 解析推荐请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ResolveReferralDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
@@ -64,41 +64,41 @@ public sealed class ResolveReferralDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录推荐事件请求 DTO。
|
||||
/// 记录推荐事件请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class TrackReferralEventDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 推荐码。
|
||||
/// 推荐码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string RefCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 事件类型。
|
||||
/// 事件类型。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? EventType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标用户 ID。
|
||||
/// 目标用户 ID。
|
||||
/// </summary>
|
||||
public Guid? TargetUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
@@ -110,25 +110,25 @@ public sealed class TrackReferralEventDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定推荐请求 DTO。
|
||||
/// 绑定推荐请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class BindReferralDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 推荐码。
|
||||
/// 推荐码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string RefCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
@@ -139,36 +139,36 @@ public sealed class BindReferralDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐二维码请求 DTO。
|
||||
/// 推荐二维码请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ReferralQrcodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 场景参数。
|
||||
/// 场景参数。
|
||||
/// </summary>
|
||||
[StringLength(128)]
|
||||
public string? Scene { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务提供方。
|
||||
/// 服务提供方。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二维码地址。
|
||||
/// 二维码地址。
|
||||
/// </summary>
|
||||
[StringLength(2048)]
|
||||
public string? QrcodeUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
@@ -179,17 +179,17 @@ public sealed class ReferralQrcodeDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐统计查询参数。
|
||||
/// 推荐统计查询参数。
|
||||
/// </summary>
|
||||
public sealed class ReferralStatsQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
public Guid? ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -201,35 +201,35 @@ public sealed class ReferralStatsQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐Conversion查询参数。
|
||||
/// 推荐Conversion查询参数。
|
||||
/// </summary>
|
||||
public sealed class ReferralConversionQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
public Guid? ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// 开始日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string? StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// 结束日期,格式为 yyyy-MM-dd。
|
||||
/// </summary>
|
||||
[RegularExpression("^\\d{4}-\\d{2}-\\d{2}$")]
|
||||
public string? EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 天数。
|
||||
/// 天数。
|
||||
/// </summary>
|
||||
[Range(1, 365)]
|
||||
public int? Days { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 100)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -253,35 +253,35 @@ public sealed class ReferralConversionQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 人工绑定推荐请求 DTO。
|
||||
/// 人工绑定推荐请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class ManualBindReferralDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 学生用户 ID。
|
||||
/// 学生用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid StudentUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 推荐人用户 ID。
|
||||
/// 推荐人用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid ReferrerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 来源。
|
||||
/// 来源。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否强制执行。
|
||||
/// 是否强制执行。
|
||||
/// </summary>
|
||||
public bool Force { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
@@ -292,12 +292,12 @@ public sealed class ManualBindReferralDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 推荐团队查询参数。
|
||||
/// 推荐团队查询参数。
|
||||
/// </summary>
|
||||
public sealed class ReferralTeamQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 上级成员用户 ID。
|
||||
/// 上级成员用户 ID。
|
||||
/// </summary>
|
||||
public Guid? LeaderUserId { get; set; }
|
||||
|
||||
@@ -308,35 +308,35 @@ public sealed class ReferralTeamQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新推荐团队请求 DTO。
|
||||
/// 新增或更新推荐团队请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class UpsertReferralTeamDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 成员用户 ID。
|
||||
/// 成员用户 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid MemberUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上级成员用户 ID。
|
||||
/// 上级成员用户 ID。
|
||||
/// </summary>
|
||||
public Guid? LeaderUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关系类型。
|
||||
/// 关系类型。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? RelationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement? Metadata { get; set; }
|
||||
|
||||
@@ -344,4 +344,4 @@ public sealed class UpsertReferralTeamDto
|
||||
{
|
||||
return new UpsertReferralTeamCommand(MemberUserId, LeaderUserId, RelationType, Status, Metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,56 +6,66 @@ using Tiku.Domain.Platform;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新SaaS功能请求 DTO。
|
||||
/// 新增或更新SaaS功能请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record UpsertSaasFeatureDto(
|
||||
Guid? Id,
|
||||
[Required, MaxLength(120)] string Code,
|
||||
[Required, MaxLength(200)] string Name,
|
||||
[Required, MaxLength(100)] string Category,
|
||||
[Required] [MaxLength(120)] string Code,
|
||||
[Required] [MaxLength(200)] string Name,
|
||||
[Required] [MaxLength(100)] string Category,
|
||||
[MaxLength(1000)] string? Description,
|
||||
[Range(0, int.MaxValue)] int ReferencePriceCents,
|
||||
[Required, MaxLength(10)] string Currency,
|
||||
[Required] [MaxLength(10)] string Currency,
|
||||
SaasFeatureStatus Status,
|
||||
int SortOrder)
|
||||
{
|
||||
public UpsertSaasFeatureCommand ToCommand() => new(Id, Code, Name, Category, Description, ReferencePriceCents, Currency, Status, SortOrder);
|
||||
public UpsertSaasFeatureCommand ToCommand()
|
||||
{
|
||||
return new UpsertSaasFeatureCommand(Id, Code, Name, Category, Description, ReferencePriceCents, Currency,
|
||||
Status, SortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新SaaS套餐请求 DTO。
|
||||
/// 新增或更新SaaS套餐请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record UpsertSaasOfferingDto(
|
||||
Guid? Id,
|
||||
[Required, MaxLength(120)] string Code,
|
||||
[Required, MaxLength(200)] string Name,
|
||||
[Required] [MaxLength(120)] string Code,
|
||||
[Required] [MaxLength(200)] string Name,
|
||||
SaasOfferingType Type,
|
||||
SaasOfferingStatus Status,
|
||||
[MaxLength(1000)] string? Description,
|
||||
int SortOrder)
|
||||
{
|
||||
public UpsertSaasOfferingCommand ToCommand() => new(Id, Code, Name, Type, Status, Description, SortOrder);
|
||||
public UpsertSaasOfferingCommand ToCommand()
|
||||
{
|
||||
return new UpsertSaasOfferingCommand(Id, Code, Name, Type, Status, Description, SortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新 SaaS 功能额度定义。
|
||||
/// 新增或更新 SaaS 功能额度定义。
|
||||
/// </summary>
|
||||
public sealed record UpsertSaasFeatureLimitDto(
|
||||
Guid? Id,
|
||||
[Required, MaxLength(120)] string MetricCode,
|
||||
[Required, MaxLength(120)] string FeatureCode,
|
||||
[Required, MaxLength(200)] string Name,
|
||||
[Required, MaxLength(50)] string Unit,
|
||||
[Required] [MaxLength(120)] string MetricCode,
|
||||
[Required] [MaxLength(120)] string FeatureCode,
|
||||
[Required] [MaxLength(200)] string Name,
|
||||
[Required] [MaxLength(50)] string Unit,
|
||||
SaasFeatureLimitKind Kind,
|
||||
[Range(1, 100)] int WarningPercent,
|
||||
bool IsHardLimit)
|
||||
{
|
||||
public UpsertSaasFeatureLimitCommand ToCommand() => new(
|
||||
Id, MetricCode, FeatureCode, Name, Unit, Kind, WarningPercent, IsHardLimit);
|
||||
public UpsertSaasFeatureLimitCommand ToCommand()
|
||||
{
|
||||
return new UpsertSaasFeatureLimitCommand(
|
||||
Id, MetricCode, FeatureCode, Name, Unit, Kind, WarningPercent, IsHardLimit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新SaaS套餐版本请求 DTO。
|
||||
/// 新增或更新SaaS套餐版本请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record UpsertSaasOfferingVersionDto(
|
||||
Guid? Id,
|
||||
@@ -63,91 +73,113 @@ public sealed record UpsertSaasOfferingVersionDto(
|
||||
PlatformBillingCycle BillingCycle,
|
||||
[Range(0, int.MaxValue)] int OriginalAmountCents,
|
||||
[Range(0, int.MaxValue)] int AmountCents,
|
||||
[Required, MaxLength(10)] string Currency,
|
||||
[Required] [MaxLength(10)] string Currency,
|
||||
DateTimeOffset? EffectiveAt,
|
||||
IReadOnlyCollection<string>? FeatureCodes,
|
||||
IReadOnlyDictionary<string, long>? Limits,
|
||||
JsonElement Metadata)
|
||||
{
|
||||
public UpsertSaasOfferingVersionCommand ToCommand() => new(
|
||||
Id, OfferingId, BillingCycle, OriginalAmountCents, AmountCents, Currency, EffectiveAt,
|
||||
FeatureCodes ?? [], Limits ?? new Dictionary<string, long>(), Metadata);
|
||||
public UpsertSaasOfferingVersionCommand ToCommand()
|
||||
{
|
||||
return new UpsertSaasOfferingVersionCommand(
|
||||
Id, OfferingId, BillingCycle, OriginalAmountCents, AmountCents, Currency, EffectiveAt,
|
||||
FeatureCodes ?? [], Limits ?? new Dictionary<string, long>(), Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建平台账务报价请求 DTO。
|
||||
/// 创建平台账务报价请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record CreatePlatformBillingQuoteDto(
|
||||
Guid BaseOfferingVersionId,
|
||||
IReadOnlyCollection<Guid>? AddOnOfferingVersionIds,
|
||||
PlatformBillingOrderPurpose Purpose,
|
||||
[Required, MaxLength(200)] string IdempotencyKey)
|
||||
[Required] [MaxLength(200)] string IdempotencyKey)
|
||||
{
|
||||
public CreatePlatformBillingQuoteCommand ToCommand() => new(BaseOfferingVersionId, AddOnOfferingVersionIds ?? [], Purpose, IdempotencyKey);
|
||||
public CreatePlatformBillingQuoteCommand ToCommand()
|
||||
{
|
||||
return new CreatePlatformBillingQuoteCommand(BaseOfferingVersionId, AddOnOfferingVersionIds ?? [], Purpose,
|
||||
IdempotencyKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建平台账务订单请求 DTO。
|
||||
/// 创建平台账务订单请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record CreatePlatformBillingOrderDto(Guid QuoteId, [Required, MaxLength(200)] string IdempotencyKey)
|
||||
public sealed record CreatePlatformBillingOrderDto(Guid QuoteId, [Required] [MaxLength(200)] string IdempotencyKey)
|
||||
{
|
||||
public CreatePlatformBillingOrderCommand ToCommand() => new(QuoteId, IdempotencyKey);
|
||||
public CreatePlatformBillingOrderCommand ToCommand()
|
||||
{
|
||||
return new CreatePlatformBillingOrderCommand(QuoteId, IdempotencyKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建平台账务支付请求 DTO。
|
||||
/// 创建平台账务支付请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record CreatePlatformBillingPaymentDto(
|
||||
[Required, MaxLength(50)] string Provider,
|
||||
[Required, MaxLength(50)] string Method,
|
||||
[Required, MaxLength(200)] string IdempotencyKey,
|
||||
[Required] [MaxLength(50)] string Provider,
|
||||
[Required] [MaxLength(50)] string Method,
|
||||
[Required] [MaxLength(200)] string IdempotencyKey,
|
||||
string? OpenId,
|
||||
string? ReturnUrl,
|
||||
string? QuitUrl)
|
||||
{
|
||||
public CreatePlatformBillingPaymentCommand ToCommand(string orderNo) =>
|
||||
new(orderNo, Provider, Method, IdempotencyKey, OpenId, ReturnUrl, QuitUrl);
|
||||
public CreatePlatformBillingPaymentCommand ToCommand(string orderNo)
|
||||
{
|
||||
return new CreatePlatformBillingPaymentCommand(orderNo, Provider, Method, IdempotencyKey, OpenId, ReturnUrl,
|
||||
QuitUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变更租户订阅请求 DTO。
|
||||
/// 变更租户订阅请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record ChangeTenantSubscriptionDto(
|
||||
Guid BaseOfferingVersionId,
|
||||
IReadOnlyCollection<Guid>? AddOnOfferingVersionIds,
|
||||
[Required, MaxLength(200)] string IdempotencyKey)
|
||||
[Required] [MaxLength(200)] string IdempotencyKey)
|
||||
{
|
||||
public ChangeTenantSubscriptionCommand ToCommand() => new(BaseOfferingVersionId, AddOnOfferingVersionIds ?? []);
|
||||
public ChangeTenantSubscriptionCommand ToCommand()
|
||||
{
|
||||
return new ChangeTenantSubscriptionCommand(BaseOfferingVersionId, AddOnOfferingVersionIds ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Idempotent租户账务请求 DTO。
|
||||
/// Idempotent租户账务请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record IdempotentTenantBillingDto([Required, MaxLength(200)] string IdempotencyKey);
|
||||
public sealed record IdempotentTenantBillingDto([Required] [MaxLength(200)] string IdempotencyKey);
|
||||
|
||||
/// <summary>
|
||||
/// 确认人工平台支付请求 DTO。
|
||||
/// 确认人工平台支付请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record ConfirmManualPlatformPaymentDto(
|
||||
Guid PaymentId,
|
||||
string? ProviderTradeNo,
|
||||
DateTimeOffset? PaidAt,
|
||||
[Required, MaxLength(1000)] string Reason)
|
||||
[Required] [MaxLength(1000)] string Reason)
|
||||
{
|
||||
public ConfirmManualPaymentCommand ToCommand() => new(PaymentId, ProviderTradeNo, PaidAt, Reason);
|
||||
public ConfirmManualPaymentCommand ToCommand()
|
||||
{
|
||||
return new ConfirmManualPaymentCommand(PaymentId, ProviderTradeNo, PaidAt, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增或更新租户功能覆盖规则请求 DTO。
|
||||
/// 新增或更新租户功能覆盖规则请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record UpsertTenantFeatureOverrideDto(
|
||||
Guid TenantId,
|
||||
[Required, MaxLength(120)] string FeatureCode,
|
||||
[Required] [MaxLength(120)] string FeatureCode,
|
||||
TenantFeatureOverrideMode Mode,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
[Required, MaxLength(1000)] string Reason)
|
||||
[Required] [MaxLength(1000)] string Reason)
|
||||
{
|
||||
public UpsertTenantFeatureOverrideCommand ToCommand() => new(TenantId, FeatureCode, Mode, ExpiresAt, Reason);
|
||||
public UpsertTenantFeatureOverrideCommand ToCommand()
|
||||
{
|
||||
return new UpsertTenantFeatureOverrideCommand(TenantId, FeatureCode, Mode, ExpiresAt, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>为已有租户补录试用订阅。</summary>
|
||||
@@ -155,38 +187,50 @@ public sealed record GrantTenantTrialDto(
|
||||
Guid TenantId,
|
||||
Guid BaseOfferingVersionId,
|
||||
[Range(1, 365)] int TrialDays,
|
||||
[Required, MaxLength(200)] string IdempotencyKey,
|
||||
[Required, MaxLength(1000)] string Reason)
|
||||
[Required] [MaxLength(200)] string IdempotencyKey,
|
||||
[Required] [MaxLength(1000)] string Reason)
|
||||
{
|
||||
public GrantTenantTrialCommand ToCommand() => new(TenantId, BaseOfferingVersionId, TrialDays, IdempotencyKey, Reason);
|
||||
public GrantTenantTrialCommand ToCommand()
|
||||
{
|
||||
return new GrantTenantTrialCommand(TenantId, BaseOfferingVersionId, TrialDays, IdempotencyKey, Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>平台修改订阅状态。</summary>
|
||||
public sealed record ChangePlatformSubscriptionDto(
|
||||
[Required, MaxLength(1000)] string Reason,
|
||||
[Required] [MaxLength(1000)] string Reason,
|
||||
[Range(1, 3650)] int? ExtendDays = null)
|
||||
{
|
||||
public ChangePlatformSubscriptionCommand ToCommand(Guid subscriptionId) => new(subscriptionId, Reason, ExtendDays);
|
||||
public ChangePlatformSubscriptionCommand ToCommand(Guid subscriptionId)
|
||||
{
|
||||
return new ChangePlatformSubscriptionCommand(subscriptionId, Reason, ExtendDays);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>申请 SaaS 退款。</summary>
|
||||
public sealed record RequestPlatformRefundDto(
|
||||
Guid PaymentId,
|
||||
[Range(1, int.MaxValue)] int AmountCents,
|
||||
[Required, MaxLength(1000)] string Reason,
|
||||
[Required, MaxLength(200)] string IdempotencyKey,
|
||||
[Required] [MaxLength(1000)] string Reason,
|
||||
[Required] [MaxLength(200)] string IdempotencyKey,
|
||||
PlatformBillingRefundSubscriptionEffect SubscriptionEffect)
|
||||
{
|
||||
public RequestPlatformRefundCommand ToCommand() => new(
|
||||
PaymentId,
|
||||
AmountCents,
|
||||
Reason,
|
||||
IdempotencyKey,
|
||||
SubscriptionEffect);
|
||||
public RequestPlatformRefundCommand ToCommand()
|
||||
{
|
||||
return new RequestPlatformRefundCommand(
|
||||
PaymentId,
|
||||
AmountCents,
|
||||
Reason,
|
||||
IdempotencyKey,
|
||||
SubscriptionEffect);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>审核或重试 SaaS 退款。</summary>
|
||||
public sealed record ReviewPlatformRefundDto([Required, MaxLength(1000)] string Reason)
|
||||
public sealed record ReviewPlatformRefundDto([Required] [MaxLength(1000)] string Reason)
|
||||
{
|
||||
public ReviewPlatformRefundCommand ToCommand(Guid refundId) => new(refundId, Reason);
|
||||
}
|
||||
public ReviewPlatformRefundCommand ToCommand(Guid refundId)
|
||||
{
|
||||
return new ReviewPlatformRefundCommand(refundId, Reason);
|
||||
}
|
||||
}
|
||||
@@ -4,61 +4,63 @@ using Tiku.Application.Scoreline;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 分数线查询参数。
|
||||
/// 分数线查询参数。
|
||||
/// </summary>
|
||||
public sealed class ScorelineQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 院校 ID。
|
||||
/// 院校 ID。
|
||||
/// </summary>
|
||||
public Guid? SchoolId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 专业 ID。
|
||||
/// 专业 ID。
|
||||
/// </summary>
|
||||
public Guid? MajorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 年份。
|
||||
/// 年份。
|
||||
/// </summary>
|
||||
[Range(1900, 3000)]
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
[Range(1, 10000)]
|
||||
public int? Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页数量。
|
||||
/// 每页数量。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 2000)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 版本化的不透明游标,仅用于游标分页接口。
|
||||
/// 版本化的不透明游标,仅用于游标分页接口。
|
||||
/// </summary>
|
||||
[StringLength(2000)]
|
||||
public string? Cursor { get; set; }
|
||||
@@ -79,4 +81,4 @@ public sealed class ScorelineQueryDto
|
||||
Limit,
|
||||
dynamicFilters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,59 +4,59 @@ using Tiku.Application.StudyContent;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Study内容查询参数。
|
||||
/// Study内容查询参数。
|
||||
/// </summary>
|
||||
public sealed class StudyContentQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区 ID。
|
||||
/// 地区 ID。
|
||||
/// </summary>
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 单元 ID。
|
||||
/// 单元 ID。
|
||||
/// </summary>
|
||||
public Guid? UnitId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 章节 ID。
|
||||
/// 章节 ID。
|
||||
/// </summary>
|
||||
public Guid? ChapterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容入口 ID。
|
||||
/// 内容入口 ID。
|
||||
/// </summary>
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容节点 ID。
|
||||
/// 内容节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ContentNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含正文内容。
|
||||
/// 是否包含正文内容。
|
||||
/// </summary>
|
||||
public bool IncludeContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 2000)]
|
||||
public int? Limit { get; set; }
|
||||
@@ -75,4 +75,4 @@ public sealed class StudyContentQueryDto
|
||||
IncludeContent,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,48 +8,59 @@ using Tiku.Domain.Content;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 创建分类节点请求 DTO。
|
||||
/// 创建分类节点请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class CreateTaxonomyNodeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 父节点 ID。
|
||||
/// 父节点 ID。
|
||||
/// </summary>
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级来源。
|
||||
/// 父级来源。
|
||||
/// </summary>
|
||||
public QuestionSource? ParentSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点Type。
|
||||
/// 节点Type。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public TaxonomyNodeType NodeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码。
|
||||
/// 编码。
|
||||
/// </summary>
|
||||
[Required, StringLength(100)]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 名称。
|
||||
/// 名称。
|
||||
/// </summary>
|
||||
[Required, StringLength(300)]
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 排序值。
|
||||
/// 排序值。
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public CreateTaxonomyNodeCommand ToCommand() => new(
|
||||
ParentId,
|
||||
ParentSource,
|
||||
NodeType,
|
||||
Code,
|
||||
Name,
|
||||
SortOrder,
|
||||
Metadata);
|
||||
}
|
||||
public CreateTaxonomyNodeCommand ToCommand()
|
||||
{
|
||||
return new CreateTaxonomyNodeCommand(
|
||||
ParentId,
|
||||
ParentSource,
|
||||
NodeType,
|
||||
Code,
|
||||
Name,
|
||||
SortOrder,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,19 +7,19 @@ using Tiku.Domain.Tenancy;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 租户解析查询参数。
|
||||
/// 租户解析查询参数。
|
||||
/// </summary>
|
||||
public sealed class TenantResolveQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 访问域名。
|
||||
/// 访问域名。
|
||||
/// </summary>
|
||||
[StringLength(253)]
|
||||
[Description("要解析的访问域名,例如 student.example.com。本地开发也可以直接传 localhost。")]
|
||||
public string? Host { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户编码。
|
||||
/// 租户编码。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[Description("租户编码;本地开发或无独立域名时使用,例如 master。")]
|
||||
@@ -27,7 +27,7 @@ public sealed class TenantResolveQueryDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户解析响应。
|
||||
/// 租户解析响应。
|
||||
/// </summary>
|
||||
/// <param name="Tenant">公开租户基础信息。</param>
|
||||
/// <param name="Branding">公开品牌配置。</param>
|
||||
@@ -35,7 +35,7 @@ public sealed class TenantResolveQueryDto
|
||||
/// <param name="AdminFeatures">面向管理端公开的功能开关。</param>
|
||||
/// <param name="PublicConfig">可公开的租户配置,不包含密钥或内部配置。</param>
|
||||
/// <summary>
|
||||
/// 租户解析Response请求 DTO。
|
||||
/// 租户解析Response请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record TenantResolveResponseDto(
|
||||
PublicTenantDto Tenant,
|
||||
@@ -45,7 +45,7 @@ public sealed record TenantResolveResponseDto(
|
||||
JsonElement PublicConfig);
|
||||
|
||||
/// <summary>
|
||||
/// 可公开给客户端的租户基础信息。
|
||||
/// 可公开给客户端的租户基础信息。
|
||||
/// </summary>
|
||||
/// <param name="Id">租户 ID。</param>
|
||||
/// <param name="Slug">租户编码。</param>
|
||||
@@ -54,7 +54,7 @@ public sealed record TenantResolveResponseDto(
|
||||
/// <param name="Mode">租户模式。</param>
|
||||
/// <param name="Host">当前匹配到的访问域名。</param>
|
||||
/// <summary>
|
||||
/// 公开租户请求 DTO。
|
||||
/// 公开租户请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record PublicTenantDto(
|
||||
Guid Id,
|
||||
@@ -65,7 +65,7 @@ public sealed record PublicTenantDto(
|
||||
string? Host);
|
||||
|
||||
/// <summary>
|
||||
/// 可公开给客户端的租户品牌信息。
|
||||
/// 可公开给客户端的租户品牌信息。
|
||||
/// </summary>
|
||||
/// <param name="BrandName">品牌名称。</param>
|
||||
/// <param name="ShortName">品牌短名称。</param>
|
||||
@@ -77,7 +77,7 @@ public sealed record PublicTenantDto(
|
||||
/// <param name="Theme">公开主题配置。</param>
|
||||
/// <param name="PublicAssets">公开资源配置。</param>
|
||||
/// <summary>
|
||||
/// 公开租户品牌请求 DTO。
|
||||
/// 公开租户品牌请求 DTO。
|
||||
/// </summary>
|
||||
public sealed record PublicTenantBrandingDto(
|
||||
string? BrandName,
|
||||
@@ -100,4 +100,4 @@ public sealed record PublicTenantBrandingDto(
|
||||
null,
|
||||
JsonDefaults.Object(),
|
||||
JsonDefaults.Object());
|
||||
}
|
||||
}
|
||||
@@ -6,47 +6,54 @@ using Tiku.Domain.Common;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 保存租户前端配置草稿请求。
|
||||
/// 保存租户前端配置草稿请求。
|
||||
/// </summary>
|
||||
public sealed class SaveTenantFrontendConfigDraftDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 品牌配置。
|
||||
/// 品牌配置。
|
||||
/// </summary>
|
||||
public JsonElement Branding { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 主题配置。
|
||||
/// 主题配置。
|
||||
/// </summary>
|
||||
public JsonElement Theme { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 功能配置。
|
||||
/// 功能配置。
|
||||
/// </summary>
|
||||
public JsonElement Features { get; set; } = JsonDefaults.Object();
|
||||
|
||||
/// <summary>
|
||||
/// 导航配置。
|
||||
/// 导航配置。
|
||||
/// </summary>
|
||||
public JsonElement Navigation { get; set; } = JsonDefaults.Array();
|
||||
|
||||
/// <summary>
|
||||
/// 首页模块配置。
|
||||
/// 首页模块配置。
|
||||
/// </summary>
|
||||
public JsonElement HomeModules { get; set; } = JsonDefaults.Array();
|
||||
|
||||
public TenantFrontendConfigDraft ToDraft() => new(
|
||||
Branding,
|
||||
Theme,
|
||||
Features,
|
||||
Navigation,
|
||||
HomeModules);
|
||||
public TenantFrontendConfigDraft ToDraft()
|
||||
{
|
||||
return new TenantFrontendConfigDraft(
|
||||
Branding,
|
||||
Theme,
|
||||
Features,
|
||||
Navigation,
|
||||
HomeModules);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布租户前端配置请求。
|
||||
/// 发布租户前端配置请求。
|
||||
/// </summary>
|
||||
public sealed class PublishTenantFrontendConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 期望配置版本。
|
||||
/// 期望配置版本。
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int ExpectedVersion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ namespace Tiku.Api.Contracts;
|
||||
public sealed class TenantLifecycleReasonDto
|
||||
{
|
||||
/// <summary>审计原因。</summary>
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ public sealed class TenantOwnerTransferDto
|
||||
public Guid TargetUserId { get; set; }
|
||||
|
||||
/// <summary>审计原因。</summary>
|
||||
[Required, StringLength(1000, MinimumLength = 3)]
|
||||
[Required]
|
||||
[StringLength(1000, MinimumLength = 3)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -5,90 +5,96 @@ using Tiku.Application.Assets;
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// 视频Search查询参数。
|
||||
/// 视频Search查询参数。
|
||||
/// </summary>
|
||||
public sealed class VideoSearchQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 科目 ID。
|
||||
/// 科目 ID。
|
||||
/// </summary>
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 200)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public VideoSearchQuery ToQuery() => new(Keyword, SubjectId, Limit);
|
||||
public VideoSearchQuery ToQuery()
|
||||
{
|
||||
return new VideoSearchQuery(Keyword, SubjectId, Limit);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频Play请求 DTO。
|
||||
/// 视频Play请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class VideoPlayDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频 ID。
|
||||
/// 视频 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid VideoId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
|
||||
public VideoPlayCommand ToCommand() => new(VideoId, QuestionId);
|
||||
public VideoPlayCommand ToCommand()
|
||||
{
|
||||
return new VideoPlayCommand(VideoId, QuestionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 视频Progress请求 DTO。
|
||||
/// 视频Progress请求 DTO。
|
||||
/// </summary>
|
||||
public sealed class VideoProgressDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 视频 ID。
|
||||
/// 视频 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Guid VideoId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 播放位置,单位为秒。
|
||||
/// 播放位置,单位为秒。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int PositionSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 时长,单位为秒。
|
||||
/// 时长,单位为秒。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? DurationSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已观看时长,单位为秒。
|
||||
/// 已观看时长,单位为秒。
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int? WatchedSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已完成。
|
||||
/// 是否已完成。
|
||||
/// </summary>
|
||||
public bool? IsCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 扩展元数据。
|
||||
/// 扩展元数据。
|
||||
/// </summary>
|
||||
public JsonElement Metadata { get; set; } = JsonSerializer.SerializeToElement(new { });
|
||||
|
||||
@@ -106,26 +112,29 @@ public sealed class VideoProgressDto
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 题目视频查询参数。
|
||||
/// 题目视频查询参数。
|
||||
/// </summary>
|
||||
public sealed class QuestionVideoQueryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 题目 ID。
|
||||
/// 题目 ID。
|
||||
/// </summary>
|
||||
public Guid? QuestionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 题目 ID 列表。
|
||||
/// 题目 ID 列表。
|
||||
/// </summary>
|
||||
[MaxLength(100)]
|
||||
public IReadOnlyCollection<Guid>? QuestionIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 返回数量上限。
|
||||
/// 返回数量上限。
|
||||
/// </summary>
|
||||
[Range(1, 500)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public QuestionVideoQuery ToQuery() => new(QuestionId, QuestionIds, Limit);
|
||||
}
|
||||
public QuestionVideoQuery ToQuery()
|
||||
{
|
||||
return new QuestionVideoQuery(QuestionId, QuestionIds, Limit);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -74,18 +73,12 @@ public sealed class AssetsController(
|
||||
|
||||
private async Task<Guid> ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value;
|
||||
|
||||
var resolvedTenantCode = tenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(resolvedTenantCode))
|
||||
{
|
||||
throw new TenantNotFoundException();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(resolvedTenantCode)) throw new TenantNotFoundException();
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(resolvedTenantCode, cancellationToken);
|
||||
return tenant?.TenantId ?? throw new TenantNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -59,12 +59,10 @@ public sealed class AuthController(
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
if (realm != AuthRealm.Tenant)
|
||||
{
|
||||
throw new RequiredFieldException("SMS authentication is only available in the tenant realm.");
|
||||
}
|
||||
|
||||
var tenantId = await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
|
||||
var result = await smsVerificationService.CreateCodeAsync(
|
||||
new SendSmsCodeRequest(
|
||||
tenantId,
|
||||
@@ -90,10 +88,7 @@ public sealed class AuthController(
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
var identifier = request.Identifier ?? request.Phone;
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
throw new RequiredFieldException("identifier is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(identifier)) throw new RequiredFieldException("identifier is required.");
|
||||
var result = await authService.LoginWithPasswordAsync(
|
||||
new PasswordLoginRequest(
|
||||
realm,
|
||||
@@ -223,10 +218,7 @@ public sealed class AuthController(
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<IActionResult> LogoutAll(CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is not { } userId) return Unauthorized();
|
||||
|
||||
await authService.LogoutAllAsync(userId, cancellationToken);
|
||||
return NoContent();
|
||||
@@ -260,7 +252,7 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = await ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
var result = await authService.RequestPasswordResetAsync(
|
||||
new PasswordResetCodeRequest(
|
||||
tenantId,
|
||||
@@ -282,7 +274,7 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = await ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
await authService.ResetPasswordAsync(
|
||||
new PasswordResetRequest(
|
||||
tenantId,
|
||||
@@ -304,10 +296,7 @@ public sealed class AuthController(
|
||||
AuthenticatedPasswordChangeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId) return Unauthorized();
|
||||
|
||||
var result = await authService.ChangePasswordAsync(
|
||||
new AuthenticatedPasswordChangeRequest(
|
||||
@@ -323,53 +312,39 @@ public sealed class AuthController(
|
||||
|
||||
private void ResolveRefreshTokenTenant(string refreshToken)
|
||||
{
|
||||
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator)) return;
|
||||
|
||||
if (locator.Realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved)
|
||||
{
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tenantContext.IsResolved)
|
||||
{
|
||||
throw new RequiredFieldException(
|
||||
"tenant refresh/logout requires a tenant host or x-tenant-code matching the refresh token.");
|
||||
}
|
||||
|
||||
tenantContextInitializer.Initialize(locator.TenantId!.Value, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
private void ResolveAuthChallengeTenant(string challengeToken)
|
||||
{
|
||||
var parts = challengeToken.Split('.', 4, StringSplitOptions.None);
|
||||
if (parts.Length != 4 || parts[0] != "c1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
var parts = challengeToken.Split('.', 4);
|
||||
if (parts.Length != 4 || parts[0] != "c1") return;
|
||||
|
||||
if (parts[1] == "p" && parts[2] == "-")
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved)
|
||||
{
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (parts[1] != "t" || !Guid.TryParseExact(parts[2], "N", out var tenantId) || !tenantContext.IsResolved)
|
||||
{
|
||||
throw new RequiredFieldException(
|
||||
"tenant authentication challenge requires a tenant host or x-tenant-code.");
|
||||
}
|
||||
|
||||
tenantContextInitializer.Initialize(tenantId, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
@@ -388,9 +363,8 @@ public sealed class AuthController(
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved || !string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new RequiredFieldException("platform realm does not accept tenantCode and must use a platform host.");
|
||||
}
|
||||
throw new RequiredFieldException(
|
||||
"platform realm does not accept tenantCode and must use a platform host.");
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -402,23 +376,19 @@ public sealed class AuthController(
|
||||
{
|
||||
var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken);
|
||||
if (supplied?.TenantId != tenantContext.TenantId.Value)
|
||||
{
|
||||
throw new TenantContextConflictException(
|
||||
tenantContext.TenantId.Value,
|
||||
supplied?.TenantId ?? Guid.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant.");
|
||||
}
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
@@ -434,8 +404,6 @@ public sealed class AuthController(
|
||||
host.Trim().TrimEnd('.'),
|
||||
requestHost,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ public sealed class BackgroundJobsController(
|
||||
return tenantContext.TenantId ?? throw new InvalidOperationException("Tenant context was not resolved.");
|
||||
}
|
||||
|
||||
private Guid ResolveUserId() =>
|
||||
currentUser.UserId ?? throw new InvalidOperationException("Current user was not resolved.");
|
||||
}
|
||||
private Guid ResolveUserId()
|
||||
{
|
||||
return currentUser.UserId ?? throw new InvalidOperationException("Current user was not resolved.");
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,9 @@ public sealed class BrowserAuthController(
|
||||
EnsureTrustedOrigin();
|
||||
var realm = request.Realm!.Value;
|
||||
if (realm != AuthRealm.Tenant)
|
||||
{
|
||||
throw new RequiredFieldException("SMS authentication is only available in the tenant realm.");
|
||||
}
|
||||
var tenantId = await ResolveTenantIdAsync(realm, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
|
||||
var result = await smsVerificationService.CreateCodeAsync(new SendSmsCodeRequest(
|
||||
tenantId,
|
||||
request.Phone,
|
||||
@@ -174,9 +172,7 @@ public sealed class BrowserAuthController(
|
||||
{
|
||||
var refreshToken = Request.Cookies[BrowserAuthOptions.RefreshCookie];
|
||||
if (!string.IsNullOrWhiteSpace(refreshToken))
|
||||
{
|
||||
await authService.LogoutAsync(new LogoutSessionRequest(refreshToken), cancellationToken);
|
||||
}
|
||||
ClearCookies();
|
||||
return NoContent();
|
||||
}
|
||||
@@ -202,7 +198,7 @@ public sealed class BrowserAuthController(
|
||||
{
|
||||
EnsureTrustedOrigin();
|
||||
var tenantId = await ResolveTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
var result = await authService.RequestPasswordResetAsync(
|
||||
new PasswordResetCodeRequest(
|
||||
tenantId,
|
||||
@@ -225,7 +221,7 @@ public sealed class BrowserAuthController(
|
||||
{
|
||||
EnsureTrustedOrigin();
|
||||
var tenantId = await ResolveTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
await authService.ResetPasswordAsync(
|
||||
new PasswordResetRequest(
|
||||
tenantId,
|
||||
@@ -248,10 +244,7 @@ public sealed class BrowserAuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureTrustedOrigin();
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId) return Unauthorized();
|
||||
|
||||
var result = await authService.ChangePasswordAsync(
|
||||
new AuthenticatedPasswordChangeRequest(
|
||||
@@ -267,22 +260,21 @@ public sealed class BrowserAuthController(
|
||||
|
||||
private object WriteResult(AuthenticationResult result)
|
||||
{
|
||||
if (result.User?.Tokens is { } tokens)
|
||||
{
|
||||
WriteCookies(tokens);
|
||||
}
|
||||
if (result.User?.Tokens is { } tokens) WriteCookies(tokens);
|
||||
return new
|
||||
{
|
||||
status = result.Status.ToString(),
|
||||
user = result.User is null ? null : new
|
||||
{
|
||||
result.User.UserId,
|
||||
result.User.Phone,
|
||||
result.User.Email,
|
||||
result.User.Name,
|
||||
result.User.Realm,
|
||||
result.User.Tenant
|
||||
},
|
||||
user = result.User is null
|
||||
? null
|
||||
: new
|
||||
{
|
||||
result.User.UserId,
|
||||
result.User.Phone,
|
||||
result.User.Email,
|
||||
result.User.Name,
|
||||
result.User.Realm,
|
||||
result.User.Tenant
|
||||
},
|
||||
result.ChallengeToken,
|
||||
result.ChallengeExpiresAt
|
||||
};
|
||||
@@ -319,7 +311,8 @@ public sealed class BrowserAuthController(
|
||||
private void ClearCookies()
|
||||
{
|
||||
Response.Cookies.Delete(BrowserAuthOptions.AccessCookie, new CookieOptions { Secure = true, Path = "/" });
|
||||
Response.Cookies.Delete(BrowserAuthOptions.RefreshCookie, new CookieOptions { Secure = true, Path = "/api/tenant/auth/browser" });
|
||||
Response.Cookies.Delete(BrowserAuthOptions.RefreshCookie,
|
||||
new CookieOptions { Secure = true, Path = "/api/tenant/auth/browser" });
|
||||
Response.Cookies.Delete(BrowserAuthOptions.CsrfCookie, new CookieOptions { Secure = true, Path = "/" });
|
||||
}
|
||||
|
||||
@@ -329,12 +322,11 @@ public sealed class BrowserAuthController(
|
||||
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri) ||
|
||||
!string.Equals(uri.Scheme, Request.Scheme, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(uri.Authority, Request.Host.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BrowserOriginException();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Guid?> ResolveTenantIdAsync(AuthRealm realm, string? tenantCode, CancellationToken cancellationToken)
|
||||
private async Task<Guid?> ResolveTenantIdAsync(AuthRealm realm, string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (realm == AuthRealm.Platform)
|
||||
{
|
||||
@@ -344,13 +336,14 @@ public sealed class BrowserAuthController(
|
||||
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tenantContext.TenantId is { } resolved) return resolved;
|
||||
if (string.IsNullOrWhiteSpace(tenantCode)) throw new RequiredFieldException("tenantCode is required.");
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class BrowserOriginException() : Exception("Browser authentication requires a same-origin request.");
|
||||
public sealed class BrowserOriginException() : Exception("Browser authentication requires a same-origin request.");
|
||||
@@ -1,15 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.StudyContent;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -148,7 +148,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("question-collections")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询可用题集")]
|
||||
[ProducesResponseType<CatalogList<QuestionCollectionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -162,7 +162,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("question-collections/questions")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询题集内题目")]
|
||||
[ProducesResponseType<CatalogList<CollectionQuestionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
@@ -177,7 +177,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("practice-blueprints")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询练习蓝图")]
|
||||
[ProducesResponseType<CatalogList<PracticeBlueprintCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -191,7 +191,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("question-banks")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询题库列表")]
|
||||
[ProducesResponseType<CatalogList<QuestionBankCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -205,7 +205,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("questions")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询已发布题目")]
|
||||
[EndpointDescription("支持按题库、科目、分类、模块节点、内容入口、内容节点、题集或题目 ID 列表筛选。")]
|
||||
[ProducesResponseType<CatalogList<QuestionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
@@ -220,7 +220,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("questions/{questionId:guid}")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询题目详情")]
|
||||
[ProducesResponseType<QuestionCatalogItem>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -235,7 +235,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("questions/{questionId:guid}/versions")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询题目版本")]
|
||||
[ProducesResponseType<CatalogList<QuestionVersionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -250,7 +250,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-units")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询词汇单元")]
|
||||
[ProducesResponseType<CatalogList<VocabularyUnitCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -264,7 +264,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-words")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询词汇单词")]
|
||||
[ProducesResponseType<CatalogList<VocabularyWordCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -278,7 +278,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("handbook-subjects")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询知识手册科目")]
|
||||
[ProducesResponseType<CatalogList<HandbookSubjectCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -292,7 +292,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("handbook-chapters")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询知识手册章节")]
|
||||
[ProducesResponseType<CatalogList<HandbookChapterCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -306,7 +306,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("handbook-entries")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询知识手册条目")]
|
||||
[ProducesResponseType<CatalogList<HandbookEntryCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -359,7 +359,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("video-explanations")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("查询视频讲解")]
|
||||
[ProducesResponseType<CatalogList<VideoExplanationCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -373,7 +373,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("question-videos")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("查询题目关联视频")]
|
||||
[ProducesResponseType<CatalogList<QuestionVideoCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -387,7 +387,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("banners")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询首页横幅")]
|
||||
[ProducesResponseType<CatalogList<BannerCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -401,7 +401,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("faqs")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询常见问题")]
|
||||
[ProducesResponseType<CatalogList<FaqCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -415,7 +415,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("announcements")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询公告")]
|
||||
[ProducesResponseType<CatalogList<AnnouncementCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -429,7 +429,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("exam-dates")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询考试日期")]
|
||||
[ProducesResponseType<CatalogList<ExamDateCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -443,7 +443,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("products")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[EndpointSummary("查询可购买产品")]
|
||||
[ProducesResponseType<CatalogList<ProductCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -457,7 +457,7 @@ public sealed class CatalogController(
|
||||
}
|
||||
|
||||
[HttpGet("svip-plans")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[EndpointSummary("查询 SVIP 套餐")]
|
||||
[ProducesResponseType<CatalogList<SvipPlanCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
@@ -474,16 +474,10 @@ public sealed class CatalogController(
|
||||
CatalogQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value;
|
||||
|
||||
var tenantCode = query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new TenantNotFoundException();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(tenantCode)) throw new TenantNotFoundException();
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode, cancellationToken);
|
||||
return tenant?.TenantId ?? throw new TenantNotFoundException();
|
||||
@@ -544,4 +538,4 @@ public sealed class TenantNotFoundException : Exception
|
||||
: base("Tenant was not found.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
@@ -14,7 +15,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("学生端-交易")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/commerce")]
|
||||
public sealed class CommerceController(
|
||||
@@ -189,18 +190,13 @@ public sealed class CommerceController(
|
||||
string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new CommerceException("Tenant code is required for payment notification.", "tenant_required");
|
||||
}
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new CommerceException("Tenant was not found.", "tenant_not_found");
|
||||
?? throw new CommerceException("Tenant was not found.", "tenant_not_found");
|
||||
tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
@@ -208,9 +204,7 @@ public sealed class CommerceController(
|
||||
private CommerceActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new CommerceException("Current commerce actor was not resolved.", "commerce_access_denied");
|
||||
}
|
||||
|
||||
return new CommerceActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
@@ -221,9 +215,7 @@ public sealed class CommerceController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
throw new CommerceException("Tenant id is required for payment notification.", "tenant_required");
|
||||
}
|
||||
|
||||
var rawBody = await ReadRawBodyAsync(Request, cancellationToken);
|
||||
using var body = ParseNotificationBody(Request, rawBody, cancellationToken);
|
||||
@@ -246,7 +238,7 @@ public sealed class CommerceController(
|
||||
using var reader = new StreamReader(
|
||||
request.Body,
|
||||
Encoding.UTF8,
|
||||
detectEncodingFromByteOrderMarks: false,
|
||||
false,
|
||||
leaveOpen: false);
|
||||
return await reader.ReadToEndAsync(cancellationToken);
|
||||
}
|
||||
@@ -267,11 +259,8 @@ public sealed class CommerceController(
|
||||
return JsonDocument.Parse(JsonSerializer.Serialize(values));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawBody))
|
||||
{
|
||||
return JsonDocument.Parse("{}");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(rawBody)) return JsonDocument.Parse("{}");
|
||||
|
||||
return JsonDocument.Parse(rawBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("租户端-佣金")]
|
||||
[Authorize(Policy = BackendPermissions.TenantCommissionManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/commission")]
|
||||
public sealed class CommissionController(
|
||||
@@ -19,71 +20,109 @@ public sealed class CommissionController(
|
||||
{
|
||||
[HttpGet("settings")]
|
||||
[EndpointSummary("查询佣金配置")]
|
||||
public async Task<ActionResult<CommissionSettingsItem>> Settings(CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GetSettingsAsync(ResolveActor(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionSettingsItem>> Settings(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GetSettingsAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("settings")]
|
||||
[EndpointSummary("保存佣金配置")]
|
||||
public async Task<ActionResult<CommissionSettingsItem>> UpdateSettings(UpdateCommissionSettingsDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.UpdateSettingsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionSettingsItem>> UpdateSettings(UpdateCommissionSettingsDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.UpdateSettingsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("member-rate")]
|
||||
[EndpointSummary("调整成员佣金比例")]
|
||||
public async Task<ActionResult<object>> MemberRate(UpdateMemberCommissionRateDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.UpdateMemberRateAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<object>> MemberRate(UpdateMemberCommissionRateDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.UpdateMemberRateAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("summary")]
|
||||
[EndpointSummary("查询佣金统计摘要")]
|
||||
public async Task<ActionResult<CommissionSummaryItem>> Summary([FromQuery] CommissionPeriodQueryDto query, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GetSummaryAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionSummaryItem>> Summary([FromQuery] CommissionPeriodQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GetSummaryAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("orders")]
|
||||
[EndpointSummary("查询佣金来源订单")]
|
||||
public async Task<ActionResult<CommissionList<CommissionSourceItem>>> Orders([FromQuery] CommissionPeriodQueryDto query, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GetOrdersAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionList<CommissionSourceItem>>> Orders(
|
||||
[FromQuery] CommissionPeriodQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GetOrdersAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("settlements")]
|
||||
[EndpointSummary("查询佣金结算单")]
|
||||
public async Task<ActionResult<CommissionList<CommissionSettlementItemDto>>> Settlements([FromQuery] CommissionSettlementsQueryDto query, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GetSettlementsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionList<CommissionSettlementItemDto>>> Settlements(
|
||||
[FromQuery] CommissionSettlementsQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GetSettlementsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("settlements/export")]
|
||||
[EndpointSummary("导出佣金结算单")]
|
||||
public async Task<ActionResult<CommissionExportItem>> Export([FromQuery] CommissionSettlementExportQueryDto query, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.ExportSettlementAsync(ResolveActor(), query.SettlementId, query.Format, cancellationToken));
|
||||
public async Task<ActionResult<CommissionExportItem>> Export([FromQuery] CommissionSettlementExportQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.ExportSettlementAsync(ResolveActor(), query.SettlementId, query.Format,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("settlements/generate")]
|
||||
[EndpointSummary("生成佣金结算单")]
|
||||
public async Task<ActionResult<CommissionSettlementItemDto>> Generate(GenerateCommissionSettlementDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GenerateSettlementAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionSettlementItemDto>> Generate(GenerateCommissionSettlementDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GenerateSettlementAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("settlements/status")]
|
||||
[EndpointSummary("更新佣金结算单状态")]
|
||||
public async Task<ActionResult<CommissionSettlementItemDto>> UpdateStatus(UpdateCommissionSettlementStatusDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.UpdateSettlementStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionSettlementItemDto>> UpdateStatus(
|
||||
UpdateCommissionSettlementStatusDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.UpdateSettlementStatusAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("settlements/proofs")]
|
||||
[EndpointSummary("查询佣金结算凭证")]
|
||||
public async Task<ActionResult<CommissionList<CommissionProofItem>>> Proofs([FromQuery] CommissionSettlementProofQueryDto query, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.GetProofsAsync(ResolveActor(), query.SettlementId, cancellationToken));
|
||||
public async Task<ActionResult<CommissionList<CommissionProofItem>>> Proofs(
|
||||
[FromQuery] CommissionSettlementProofQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.GetProofsAsync(ResolveActor(), query.SettlementId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("settlements/proofs")]
|
||||
[EndpointSummary("创建佣金结算凭证")]
|
||||
public async Task<ActionResult<CommissionProofItem>> CreateProof(CreateCommissionProofDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.CreateProofAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionProofItem>> CreateProof(CreateCommissionProofDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commissionService.CreateProofAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("settlements/proofs/status")]
|
||||
[EndpointSummary("更新佣金结算凭证状态")]
|
||||
public async Task<ActionResult<CommissionProofItem>> UpdateProofStatus(UpdateCommissionProofStatusDto request, CancellationToken cancellationToken) =>
|
||||
Ok(await commissionService.UpdateProofStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<CommissionProofItem>> UpdateProofStatus(UpdateCommissionProofStatusDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await commissionService.UpdateProofStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
private CommissionAdminActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new CommissionException("Commission admin actor was not resolved.", "commission_access_denied");
|
||||
}
|
||||
|
||||
return new CommissionAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("租户端-CRM")]
|
||||
[Authorize(Policy = BackendPermissions.TenantCrmManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Crm)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Crm)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/crm")]
|
||||
public sealed class CrmController(
|
||||
@@ -78,10 +79,8 @@ public sealed class CrmController(
|
||||
private CrmAdminActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new CrmException("CRM admin actor was not resolved.", "crm_access_denied");
|
||||
}
|
||||
|
||||
return new CrmAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,4 @@ public sealed class HealthController(IDependencyReadinessProbe readinessProbe) :
|
||||
var response = new { status = readiness.Ready ? "ready" : "not_ready", readiness.CheckedAt };
|
||||
return readiness.Ready ? Ok(response) : StatusCode(StatusCodes.Status503ServiceUnavailable, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("学生端-学习")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/learning")]
|
||||
public sealed class LearningController(
|
||||
@@ -42,7 +43,8 @@ public sealed class LearningController(
|
||||
[FromQuery] LearningLimitQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await learningActivityService.GetLeaderboardAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(
|
||||
await learningActivityService.GetLeaderboardAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("practice-sessions")]
|
||||
@@ -306,14 +308,11 @@ public sealed class LearningController(
|
||||
|
||||
private LearningActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new LearningAccessDeniedException();
|
||||
}
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null) throw new LearningAccessDeniedException();
|
||||
|
||||
return new LearningActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LearningAccessDeniedException()
|
||||
: Exception("Learning actor was not resolved.");
|
||||
: Exception("Learning actor was not resolved.");
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
@@ -20,16 +20,10 @@ public sealed class MeController(
|
||||
[EndpointDescription("根据 Bearer Token 返回当前用户基础信息和活跃租户成员摘要。")]
|
||||
public async Task<ActionResult<MeResponse>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is null) return Unauthorized();
|
||||
|
||||
var user = await identityQueries.GetUserAsync(currentUser.UserId.Value, cancellationToken);
|
||||
if (user is null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (user is null) return Unauthorized();
|
||||
|
||||
return Ok(new MeResponse(
|
||||
user.UserId,
|
||||
@@ -50,10 +44,7 @@ public sealed class MeController(
|
||||
public async Task<ActionResult<IReadOnlyCollection<AuthSessionSummary>>> Sessions(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId) return Unauthorized();
|
||||
|
||||
return Ok(await sessionStore.ListActiveAsync(userId, sessionId, cancellationToken));
|
||||
}
|
||||
@@ -65,10 +56,7 @@ public sealed class MeController(
|
||||
Guid sessionFamilyId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
if (currentUser.UserId is not { } userId || currentUser.SessionId is not { } sessionId) return Unauthorized();
|
||||
|
||||
await sessionStore.RevokeOwnedFamilyAsync(userId, sessionId, sessionFamilyId, cancellationToken);
|
||||
return NoContent();
|
||||
@@ -76,7 +64,7 @@ public sealed class MeController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户基础信息和租户成员摘要。
|
||||
/// 当前用户基础信息和租户成员摘要。
|
||||
/// </summary>
|
||||
/// <param name="UserId">用户 ID。</param>
|
||||
/// <param name="Phone">手机号。</param>
|
||||
@@ -91,7 +79,7 @@ public sealed record MeResponse(
|
||||
IReadOnlyCollection<TenantMembershipResponse> Tenants);
|
||||
|
||||
/// <summary>
|
||||
/// 用户在某个租户内的成员摘要。
|
||||
/// 用户在某个租户内的成员摘要。
|
||||
/// </summary>
|
||||
/// <param name="TenantId">租户 ID。</param>
|
||||
/// <param name="TenantName">租户名称。</param>
|
||||
@@ -103,4 +91,4 @@ public sealed record TenantMembershipResponse(
|
||||
string TenantName,
|
||||
string TenantSlug,
|
||||
TenantRole Role,
|
||||
MembershipStatus Status);
|
||||
MembershipStatus Status);
|
||||
@@ -2,11 +2,11 @@ using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Api.OpenApi;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Api.OpenApi;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -46,7 +46,8 @@ public sealed class PlatformAdminController(
|
||||
[ProducesResponseType<PlatformTenantProvisioningResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PlatformTenantProvisioningResult>> CreateTenant(
|
||||
CreatePlatformTenantDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.CreateTenantAsync(
|
||||
@@ -61,8 +62,11 @@ public sealed class PlatformAdminController(
|
||||
public Task<PlatformTenantDomainItem> ReplacePrimaryDomain(
|
||||
Guid tenantId,
|
||||
ReplacePlatformPrimaryDomainDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
platformAdminService.ReplacePrimaryDomainAsync(ResolveActor(), request.ToCommand(tenantId), cancellationToken);
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return platformAdminService.ReplacePrimaryDomainAsync(ResolveActor(), request.ToCommand(tenantId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("tenants/{tenantId:guid}/owner-activation-links")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
|
||||
@@ -71,16 +75,21 @@ public sealed class PlatformAdminController(
|
||||
public Task<PlatformOwnerActivationLinkResult> IssueOwnerActivationLink(
|
||||
Guid tenantId,
|
||||
IssuePlatformOwnerActivationLinkDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
CancellationToken cancellationToken) =>
|
||||
platformAdminService.IssueOwnerActivationLinkAsync(
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return platformAdminService.IssueOwnerActivationLinkAsync(
|
||||
ResolveActor(), request.ToCommand(tenantId, idempotencyKey), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("tenants/{tenantId:guid}/billing-policy")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
|
||||
[EndpointSummary("查询租户收款策略")]
|
||||
public Task<TenantBillingPolicyItem> GetBillingPolicy(Guid tenantId, CancellationToken cancellationToken) =>
|
||||
platformAdminService.GetTenantBillingPolicyAsync(ResolveActor(), tenantId, cancellationToken);
|
||||
public Task<TenantBillingPolicyItem> GetBillingPolicy(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
return platformAdminService.GetTenantBillingPolicyAsync(ResolveActor(), tenantId, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("tenants/{tenantId:guid}/billing-policy")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
|
||||
@@ -88,8 +97,11 @@ public sealed class PlatformAdminController(
|
||||
public Task<TenantBillingPolicyItem> UpsertBillingPolicy(
|
||||
Guid tenantId,
|
||||
UpsertTenantBillingPolicyDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
platformAdminService.UpsertTenantBillingPolicyAsync(ResolveActor(), request.ToCommand(tenantId), cancellationToken);
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return platformAdminService.UpsertTenantBillingPolicyAsync(ResolveActor(), request.ToCommand(tenantId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("tenants/detail")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformTenantManage)]
|
||||
@@ -110,10 +122,12 @@ public sealed class PlatformAdminController(
|
||||
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> TenantStatus(
|
||||
UpdatePlatformTenantStatusDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await approvalService.UpdateTenantStatusAsync(ResolveActor(), request.ToCommand(), idempotencyKey, cancellationToken);
|
||||
var result = await approvalService.UpdateTenantStatusAsync(ResolveActor(), request.ToCommand(), idempotencyKey,
|
||||
cancellationToken);
|
||||
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
|
||||
}
|
||||
|
||||
@@ -125,7 +139,8 @@ public sealed class PlatformAdminController(
|
||||
UpsertPlatformTenantBillingProfileDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.UpsertTenantBillingProfileAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.UpsertTenantBillingProfileAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("domains")]
|
||||
@@ -180,7 +195,8 @@ public sealed class PlatformAdminController(
|
||||
UpdatePlatformStaffStatusDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.UpdateStaffStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.UpdateStaffStatusAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("staff/{userId:guid}/password-reset")]
|
||||
@@ -234,7 +250,8 @@ public sealed class PlatformAdminController(
|
||||
UpdatePlatformAuditAlertStatusDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.UpdateAuditAlertStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.UpdateAuditAlertStatusAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("saas/dunning/channels")]
|
||||
@@ -245,7 +262,8 @@ public sealed class PlatformAdminController(
|
||||
[FromQuery] PlatformAdminQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.GetBillingDunningChannelsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
return Ok(await platformAdminService.GetBillingDunningChannelsAsync(ResolveActor(), query.ToQuery(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("saas/dunning/channels")]
|
||||
@@ -256,7 +274,8 @@ public sealed class PlatformAdminController(
|
||||
UpsertPlatformBillingDunningChannelDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.UpsertBillingDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.UpsertBillingDunningChannelAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("saas/dunning/channels/disable")]
|
||||
@@ -267,7 +286,8 @@ public sealed class PlatformAdminController(
|
||||
DisablePlatformBillingDunningChannelDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.DisableBillingDunningChannelAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.DisableBillingDunningChannelAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("saas/dunning/events")]
|
||||
@@ -278,7 +298,8 @@ public sealed class PlatformAdminController(
|
||||
[FromQuery] PlatformAdminQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.GetBillingDunningEventsAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
return Ok(await platformAdminService.GetBillingDunningEventsAsync(ResolveActor(), query.ToQuery(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("saas/dunning/events/detail")]
|
||||
@@ -289,7 +310,8 @@ public sealed class PlatformAdminController(
|
||||
[FromQuery] Guid eventId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.GetBillingDunningEventDetailAsync(ResolveActor(), eventId, cancellationToken));
|
||||
return Ok(await platformAdminService.GetBillingDunningEventDetailAsync(ResolveActor(), eventId,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("saas/dunning/events/retry")]
|
||||
@@ -300,7 +322,8 @@ public sealed class PlatformAdminController(
|
||||
RetryPlatformBillingDunningEventDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.RetryBillingDunningEventAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.RetryBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("saas/dunning/events/acknowledge")]
|
||||
@@ -310,7 +333,8 @@ public sealed class PlatformAdminController(
|
||||
ResolvePlatformBillingDunningEventDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.AcknowledgeBillingDunningEventAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.AcknowledgeBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("saas/dunning/events/ignore")]
|
||||
@@ -320,16 +344,15 @@ public sealed class PlatformAdminController(
|
||||
ResolvePlatformBillingDunningEventDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await platformAdminService.IgnoreBillingDunningEventAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await platformAdminService.IgnoreBillingDunningEventAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private PlatformAdminActor ResolveActor()
|
||||
{
|
||||
if (currentUser.UserId is not { } userId)
|
||||
{
|
||||
throw new PlatformAdminException("Platform admin actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
|
||||
return new PlatformAdminActor(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,24 @@ public sealed class PlatformApprovalsController(
|
||||
public async Task<IReadOnlyCollection<PlatformApprovalRequestItem>> Requests(
|
||||
PlatformApprovalRequestStatus? status,
|
||||
int limit = 100,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await approvalService.ListAsync(await ActorAsync(cancellationToken), status, limit, cancellationToken);
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await approvalService.ListAsync(await ActorAsync(cancellationToken), status, limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("{requestId:guid}")]
|
||||
[EndpointSummary("查询平台审批详情")]
|
||||
public async Task<PlatformApprovalRequestItem> Details(Guid requestId, CancellationToken cancellationToken) =>
|
||||
await approvalService.GetAsync(await ActorAsync(cancellationToken), requestId, cancellationToken);
|
||||
public async Task<PlatformApprovalRequestItem> Details(Guid requestId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.GetAsync(await ActorAsync(cancellationToken), requestId, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("policies")]
|
||||
[EndpointSummary("查询平台审批策略")]
|
||||
public async Task<IReadOnlyCollection<PlatformApprovalPolicyItem>> Policies(CancellationToken cancellationToken) =>
|
||||
await approvalService.ListPoliciesAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformApprovalPolicyItem>> Policies(CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.ListPoliciesAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("policies/{code}")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformApprovalPolicyManage)]
|
||||
@@ -41,27 +47,42 @@ public sealed class PlatformApprovalsController(
|
||||
public async Task<PlatformApprovalPolicyItem> UpdatePolicy(
|
||||
string code,
|
||||
UpdatePlatformApprovalPolicyDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
await approvalService.UpdatePolicyAsync(await ActorAsync(cancellationToken), request.ToCommand(code), cancellationToken);
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.UpdatePolicyAsync(await ActorAsync(cancellationToken), request.ToCommand(code),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("{requestId:guid}/approve")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformApprovalDecide)]
|
||||
[EndpointSummary("批准并执行平台审批任务")]
|
||||
[PlatformOperationRisk("high")]
|
||||
public async Task<PlatformApprovalRequestItem> Approve(Guid requestId, PlatformApprovalDecisionDto request, CancellationToken cancellationToken) =>
|
||||
await approvalService.ApproveAsync(await ActorAsync(cancellationToken), requestId, request.Reason, cancellationToken);
|
||||
public async Task<PlatformApprovalRequestItem> Approve(Guid requestId, PlatformApprovalDecisionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.ApproveAsync(await ActorAsync(cancellationToken), requestId, request.Reason,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("{requestId:guid}/reject")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformApprovalDecide)]
|
||||
[EndpointSummary("拒绝平台审批任务")]
|
||||
[PlatformOperationRisk("high")]
|
||||
public async Task<PlatformApprovalRequestItem> Reject(Guid requestId, PlatformApprovalDecisionDto request, CancellationToken cancellationToken) =>
|
||||
await approvalService.RejectAsync(await ActorAsync(cancellationToken), requestId, request.Reason, cancellationToken);
|
||||
public async Task<PlatformApprovalRequestItem> Reject(Guid requestId, PlatformApprovalDecisionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.RejectAsync(await ActorAsync(cancellationToken), requestId, request.Reason,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("{requestId:guid}/cancel")]
|
||||
[EndpointSummary("撤销本人提交的平台审批任务")]
|
||||
public async Task<PlatformApprovalRequestItem> Cancel(Guid requestId, PlatformApprovalDecisionDto request, CancellationToken cancellationToken) =>
|
||||
await approvalService.CancelAsync(await ActorAsync(cancellationToken), requestId, request.Reason, cancellationToken);
|
||||
public async Task<PlatformApprovalRequestItem> Cancel(Guid requestId, PlatformApprovalDecisionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await approvalService.CancelAsync(await ActorAsync(cancellationToken), requestId, request.Reason,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PlatformApprovalActor> ActorAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -70,4 +91,4 @@ public sealed class PlatformApprovalsController(
|
||||
? new PlatformApprovalActor(userId, access.PlatformPermissions)
|
||||
: throw new PlatformApprovalException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Application.Security;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tiku.Api.OpenApi;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -35,7 +35,8 @@ public sealed class PlatformBackofficeController(
|
||||
[ProducesResponseType<BackofficeBootstrap>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<BackofficeBootstrap>> GetBootstrap(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backofficeService.GetPlatformBootstrapAsync(await ResolveActorAsync(cancellationToken), cancellationToken));
|
||||
return Ok(await backofficeService.GetPlatformBootstrapAsync(await ResolveActorAsync(cancellationToken),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("roles")]
|
||||
@@ -46,7 +47,8 @@ public sealed class PlatformBackofficeController(
|
||||
UpsertBackofficeRoleDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backofficeService.UpsertPlatformRoleAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(), cancellationToken));
|
||||
return Ok(await backofficeService.UpsertPlatformRoleAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("roles/{roleId:guid}/bindings")]
|
||||
@@ -58,10 +60,12 @@ public sealed class PlatformBackofficeController(
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> ReplaceRoleBindings(
|
||||
Guid roleId,
|
||||
ReplaceRoleBindingsDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await approvalService.ReplaceRoleBindingsAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(roleId), idempotencyKey, cancellationToken);
|
||||
var result = await approvalService.ReplaceRoleBindingsAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(roleId), idempotencyKey, cancellationToken);
|
||||
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
|
||||
}
|
||||
|
||||
@@ -74,7 +78,8 @@ public sealed class PlatformBackofficeController(
|
||||
ReplaceUserRolesDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await backofficeService.ReplacePlatformUserRolesAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(userId), cancellationToken);
|
||||
await backofficeService.ReplacePlatformUserRolesAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(userId), cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -82,4 +87,4 @@ public sealed class PlatformBackofficeController(
|
||||
{
|
||||
return BackofficeActor.FromPlatformAccess(await currentAccessContext.GetAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,16 +33,15 @@ public sealed class PlatformBillingCallbackController(
|
||||
{
|
||||
body = JsonDocument.Parse("{}").RootElement.Clone();
|
||||
}
|
||||
|
||||
var normalizedProvider = provider.Trim().ToLowerInvariant().Replace('-', '_');
|
||||
await notificationService.ProcessAsync(new PlatformBillingNotification(
|
||||
normalizedProvider,
|
||||
Request.Headers.ToDictionary(value => value.Key, value => value.Value.ToString(), StringComparer.OrdinalIgnoreCase),
|
||||
Request.Headers.ToDictionary(value => value.Key, value => value.Value.ToString(),
|
||||
StringComparer.OrdinalIgnoreCase),
|
||||
rawBody,
|
||||
body), cancellationToken);
|
||||
if (normalizedProvider is "alipay" or "ali_pay")
|
||||
{
|
||||
return Content("success", "text/plain", Encoding.UTF8);
|
||||
}
|
||||
if (normalizedProvider is "alipay" or "ali_pay") return Content("success", "text/plain", Encoding.UTF8);
|
||||
return Ok(new { code = "SUCCESS", message = "成功" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,65 +19,105 @@ public sealed class PlatformGovernanceController(
|
||||
[HttpGet("configuration/definitions")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformConfigurationManage)]
|
||||
[EndpointSummary("查询类型化平台配置定义")]
|
||||
public async Task<IReadOnlyCollection<PlatformConfigurationDefinitionItem>> ConfigurationDefinitions(CancellationToken cancellationToken) =>
|
||||
await governanceService.GetConfigurationDefinitionsAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformConfigurationDefinitionItem>> ConfigurationDefinitions(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.GetConfigurationDefinitionsAsync(await ActorAsync(cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("configuration/versions")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformConfigurationManage)]
|
||||
[EndpointSummary("查询平台配置版本")]
|
||||
public async Task<IReadOnlyCollection<PlatformConfigurationVersionItem>> ConfigurationVersions(string definitionCode, string? environment, CancellationToken cancellationToken) =>
|
||||
await governanceService.GetConfigurationVersionsAsync(await ActorAsync(cancellationToken), definitionCode, environment, cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformConfigurationVersionItem>> ConfigurationVersions(
|
||||
string definitionCode, string? environment, CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.GetConfigurationVersionsAsync(await ActorAsync(cancellationToken),
|
||||
definitionCode, environment, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("configuration/drafts")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformConfigurationManage)]
|
||||
[EndpointSummary("保存平台配置草稿")]
|
||||
public async Task<PlatformConfigurationVersionItem> SaveConfigurationDraft(SavePlatformConfigurationDraftDto request, CancellationToken cancellationToken) =>
|
||||
await governanceService.SaveConfigurationDraftAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
|
||||
public async Task<PlatformConfigurationVersionItem> SaveConfigurationDraft(
|
||||
SavePlatformConfigurationDraftDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.SaveConfigurationDraftAsync(await ActorAsync(cancellationToken),
|
||||
request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("configuration/versions/{versionId:guid}/publish")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformConfigurationManage)]
|
||||
[EndpointSummary("发布平台配置版本")]
|
||||
[PlatformOperationRisk("high")]
|
||||
public async Task<PlatformConfigurationVersionItem> PublishConfiguration(Guid versionId, CancellationToken cancellationToken) =>
|
||||
await governanceService.PublishConfigurationAsync(await ActorAsync(cancellationToken), versionId, cancellationToken);
|
||||
public async Task<PlatformConfigurationVersionItem> PublishConfiguration(Guid versionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.PublishConfigurationAsync(await ActorAsync(cancellationToken), versionId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("configuration/versions/{versionId:guid}/rollback")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformConfigurationManage)]
|
||||
[EndpointSummary("回滚平台配置版本")]
|
||||
[PlatformOperationRisk("high")]
|
||||
public async Task<PlatformConfigurationVersionItem> RollbackConfiguration(Guid versionId, PlatformRollbackDto request, CancellationToken cancellationToken) =>
|
||||
await governanceService.RollbackConfigurationAsync(await ActorAsync(cancellationToken), versionId, request.Reason, cancellationToken);
|
||||
public async Task<PlatformConfigurationVersionItem> RollbackConfiguration(Guid versionId,
|
||||
PlatformRollbackDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.RollbackConfigurationAsync(await ActorAsync(cancellationToken), versionId,
|
||||
request.Reason, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("notifications/templates")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformNotificationManage)]
|
||||
[EndpointSummary("查询平台通知模板")]
|
||||
public async Task<IReadOnlyCollection<PlatformNotificationTemplateItem>> NotificationTemplates(CancellationToken cancellationToken) =>
|
||||
await governanceService.GetNotificationTemplatesAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformNotificationTemplateItem>> NotificationTemplates(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.GetNotificationTemplatesAsync(await ActorAsync(cancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("notifications/templates")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformNotificationManage)]
|
||||
[EndpointSummary("新增或更新平台通知模板")]
|
||||
public async Task<PlatformNotificationTemplateItem> UpsertNotificationTemplate(UpsertPlatformNotificationTemplateDto request, CancellationToken cancellationToken) =>
|
||||
await governanceService.UpsertNotificationTemplateAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
|
||||
public async Task<PlatformNotificationTemplateItem> UpsertNotificationTemplate(
|
||||
UpsertPlatformNotificationTemplateDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.UpsertNotificationTemplateAsync(await ActorAsync(cancellationToken),
|
||||
request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("notifications/send")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformNotificationManage)]
|
||||
[EndpointSummary("按平台岗位发送通知")]
|
||||
public async Task<IReadOnlyCollection<PlatformNotificationDeliveryItem>> SendNotification(SendPlatformNotificationDto request, CancellationToken cancellationToken) =>
|
||||
await governanceService.SendNotificationAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformNotificationDeliveryItem>> SendNotification(
|
||||
SendPlatformNotificationDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.SendNotificationAsync(await ActorAsync(cancellationToken), request.ToCommand(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("notifications/deliveries")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformNotificationManage)]
|
||||
[EndpointSummary("分页查询平台通知投递")]
|
||||
public async Task<PagedResult<PlatformNotificationDeliveryItem>> NotificationDeliveries(
|
||||
int page = 1, int pageSize = 50, string? search = null, PlatformNotificationDeliveryStatus? status = null, CancellationToken cancellationToken = default) =>
|
||||
await governanceService.GetNotificationDeliveriesAsync(await ActorAsync(cancellationToken), new PagedQuery(page, pageSize, search), status, cancellationToken);
|
||||
int page = 1, int pageSize = 50, string? search = null, PlatformNotificationDeliveryStatus? status = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await governanceService.GetNotificationDeliveriesAsync(await ActorAsync(cancellationToken),
|
||||
new PagedQuery(page, pageSize, search), status, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("notifications/deliveries/{deliveryId:guid}/retry")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformNotificationManage)]
|
||||
[EndpointSummary("重试平台通知投递")]
|
||||
public async Task<PlatformNotificationDeliveryItem> RetryNotification(Guid deliveryId, CancellationToken cancellationToken) =>
|
||||
await governanceService.RetryNotificationAsync(await ActorAsync(cancellationToken), deliveryId, cancellationToken);
|
||||
public async Task<PlatformNotificationDeliveryItem> RetryNotification(Guid deliveryId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await governanceService.RetryNotificationAsync(await ActorAsync(cancellationToken), deliveryId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<PlatformApprovalActor> ActorAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -86,4 +126,4 @@ public sealed class PlatformGovernanceController(
|
||||
? new PlatformApprovalActor(userId, access.PlatformPermissions)
|
||||
: throw new PlatformApprovalException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.PlatformAdmin.Operations;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Operations;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
@@ -95,9 +95,11 @@ public sealed class PlatformOperationsController(
|
||||
[FromQuery] string? jobType,
|
||||
[FromQuery] BackgroundJobStatus? status,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await backgroundJobService.ListPlatformAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backgroundJobService.ListPlatformAsync(
|
||||
tenantId, jobType, status, limit ?? 100, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("jobs/{jobId:guid}")]
|
||||
[EndpointSummary("查询平台后台任务详情")]
|
||||
@@ -113,16 +115,22 @@ public sealed class PlatformOperationsController(
|
||||
public async Task<ActionResult<BackgroundJobItem>> CancelJob(
|
||||
Guid jobId,
|
||||
CancelBackgroundJobDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await backgroundJobService.RequestCancellationAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backgroundJobService.RequestCancellationAsync(
|
||||
jobId, null, ResolveUserId(), request.Reason, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("jobs/{jobId:guid}/retry")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformOperationsManage)]
|
||||
[EndpointSummary("重试平台后台任务")]
|
||||
public async Task<ActionResult<BackgroundJobItem>> RetryJob(Guid jobId, CancellationToken cancellationToken) =>
|
||||
Ok(await backgroundJobService.RetryAsync(jobId, null, ResolveUserId(), cancellationToken));
|
||||
public async Task<ActionResult<BackgroundJobItem>> RetryJob(Guid jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backgroundJobService.RetryAsync(jobId, null, ResolveUserId(), cancellationToken));
|
||||
}
|
||||
|
||||
private Guid ResolveUserId() =>
|
||||
currentUser.UserId ?? throw new InvalidOperationException("Current platform user was not resolved.");
|
||||
}
|
||||
private Guid ResolveUserId()
|
||||
{
|
||||
return currentUser.UserId ?? throw new InvalidOperationException("Current platform user was not resolved.");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.OpenApi;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Platform;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tiku.Api.OpenApi;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -22,20 +22,28 @@ public sealed class PlatformPaymentSettingsController(
|
||||
[HttpGet("apps")]
|
||||
[EndpointSummary("查询平台支付应用")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<IReadOnlyCollection<PlatformPaymentApp>> Apps(string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
paymentSettingsService.GetAppsAsync(Actor(), status, limit, cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformPaymentApp>> Apps(string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return paymentSettingsService.GetAppsAsync(Actor(), status, limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("apps")]
|
||||
[EndpointSummary("新增或更新平台支付应用")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentWrite)]
|
||||
public Task<PlatformPaymentApp> UpsertApp(UpsertPlatformPaymentAppDto request, CancellationToken cancellationToken) =>
|
||||
paymentSettingsService.UpsertAppAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<PlatformPaymentApp> UpsertApp(UpsertPlatformPaymentAppDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentSettingsService.UpsertAppAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("channels")]
|
||||
[EndpointSummary("查询平台支付渠道")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<IReadOnlyCollection<PlatformPaymentChannel>> Channels(Guid? appId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
paymentSettingsService.GetChannelsAsync(Actor(), appId, status, limit, cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformPaymentChannel>> Channels(Guid? appId, string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return paymentSettingsService.GetChannelsAsync(Actor(), appId, status, limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("channels")]
|
||||
[EndpointSummary("新增或更新平台支付渠道")]
|
||||
@@ -45,32 +53,45 @@ public sealed class PlatformPaymentSettingsController(
|
||||
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> UpsertChannel(
|
||||
UpsertPlatformPaymentChannelDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await approvalService.UpsertPaymentChannelAsync(Actor(), request.ToCommand(), idempotencyKey, cancellationToken);
|
||||
var result =
|
||||
await approvalService.UpsertPaymentChannelAsync(Actor(), request.ToCommand(), idempotencyKey,
|
||||
cancellationToken);
|
||||
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("channels/{id:guid}/disable")]
|
||||
[EndpointSummary("禁用平台支付渠道")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentWrite)]
|
||||
public Task<PlatformPaymentChannel> DisableChannel(Guid id, CancellationToken cancellationToken) =>
|
||||
paymentSettingsService.DisableChannelAsync(Actor(), id, cancellationToken);
|
||||
public Task<PlatformPaymentChannel> DisableChannel(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentSettingsService.DisableChannelAsync(Actor(), id, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("events")]
|
||||
[EndpointSummary("查询平台支付事件")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingPaymentEvent>> Events(string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
paymentSettingsService.GetEventsAsync(Actor(), status, limit, cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingPaymentEvent>> Events(string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return paymentSettingsService.GetEventsAsync(Actor(), status, limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("rebates/summary")]
|
||||
[EndpointSummary("查询平台返佣汇总")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<PlatformRebateSummary> RebateSummary(CancellationToken cancellationToken) =>
|
||||
paymentSettingsService.GetRebateSummaryAsync(Actor(), cancellationToken);
|
||||
public Task<PlatformRebateSummary> RebateSummary(CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentSettingsService.GetRebateSummaryAsync(Actor(), cancellationToken);
|
||||
}
|
||||
|
||||
private PlatformCapabilityActor Actor() => currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
private PlatformCapabilityActor Actor()
|
||||
{
|
||||
return currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ public sealed class PlatformQuestionBanksController(
|
||||
[FromQuery] string? status,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetBanksAsync(ResolveActor(), new PlatformQuestionBankFilter(Keyword: keyword, Status: status), cancellationToken));
|
||||
return Ok(await service.GetBanksAsync(ResolveActor(),
|
||||
new PlatformQuestionBankFilter(Keyword: keyword, Status: status), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
@@ -37,14 +38,16 @@ public sealed class PlatformQuestionBanksController(
|
||||
|
||||
[HttpPost("{bankId:guid}/archive")]
|
||||
[EndpointSummary("归档平台公共题库")]
|
||||
public async Task<ActionResult<PlatformQuestionBankItem>> ArchiveBank(Guid bankId, CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<PlatformQuestionBankItem>> ArchiveBank(Guid bankId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.ArchiveBankAsync(ResolveActor(), bankId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("{bankId:guid}/nodes")]
|
||||
[EndpointSummary("查询公共题库内容结构")]
|
||||
public async Task<ActionResult<IReadOnlyCollection<PlatformQuestionBankNodeItem>>> GetNodes(Guid bankId, CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<IReadOnlyCollection<PlatformQuestionBankNodeItem>>> GetNodes(Guid bankId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetNodesAsync(ResolveActor(), bankId, cancellationToken));
|
||||
}
|
||||
@@ -69,7 +72,8 @@ public sealed class PlatformQuestionBanksController(
|
||||
|
||||
[HttpPost("nodes/{nodeId:guid}/archive")]
|
||||
[EndpointSummary("归档公共题库内容节点")]
|
||||
public async Task<ActionResult<PlatformQuestionBankNodeItem>> ArchiveNode(Guid nodeId, CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<PlatformQuestionBankNodeItem>> ArchiveNode(Guid nodeId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.ArchiveNodeAsync(ResolveActor(), nodeId, cancellationToken));
|
||||
}
|
||||
@@ -149,7 +153,8 @@ public sealed class PlatformQuestionBanksController(
|
||||
AssetUploadConfirmDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.ConfirmQuestionAssetUploadAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await service.ConfirmQuestionAssetUploadAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private PlatformAdminActor ResolveActor()
|
||||
@@ -158,4 +163,4 @@ public sealed class PlatformQuestionBanksController(
|
||||
? new PlatformAdminActor(userId)
|
||||
: throw new PlatformAdminException("无法识别当前平台员工。", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.PlatformBilling;
|
||||
using Tiku.Api.OpenApi;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.PlatformBilling;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Platform;
|
||||
using Tiku.Api.OpenApi;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -23,94 +23,139 @@ public sealed class PlatformSaasController(
|
||||
[HttpGet("catalog")]
|
||||
[EndpointSummary("查询平台 SaaS 商品目录")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasCatalogSnapshot> Catalog(CancellationToken cancellationToken) =>
|
||||
catalogService.GetCatalogAsync(Actor(), cancellationToken);
|
||||
public Task<SaasCatalogSnapshot> Catalog(CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.GetCatalogAsync(Actor(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("features")]
|
||||
[EndpointSummary("新增或更新 SaaS 功能")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasFeature> UpsertFeature(UpsertSaasFeatureDto request, CancellationToken cancellationToken) =>
|
||||
catalogService.UpsertFeatureAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<SaasFeature> UpsertFeature(UpsertSaasFeatureDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.UpsertFeatureAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("feature-limits")]
|
||||
[EndpointSummary("新增或更新 SaaS 功能限额")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasFeatureLimitDefinition> UpsertFeatureLimit(
|
||||
UpsertSaasFeatureLimitDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
catalogService.UpsertLimitDefinitionAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.UpsertLimitDefinitionAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("offerings")]
|
||||
[EndpointSummary("新增或更新 SaaS 套餐")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasOffering> UpsertOffering(UpsertSaasOfferingDto request, CancellationToken cancellationToken) =>
|
||||
catalogService.UpsertOfferingAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<SaasOffering> UpsertOffering(UpsertSaasOfferingDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.UpsertOfferingAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("offering-versions")]
|
||||
[EndpointSummary("新增或更新 SaaS 套餐版本草稿")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasOfferingVersionItem> UpsertVersion(UpsertSaasOfferingVersionDto request, CancellationToken cancellationToken) =>
|
||||
catalogService.UpsertDraftVersionAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<SaasOfferingVersionItem> UpsertVersion(UpsertSaasOfferingVersionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.UpsertDraftVersionAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("offering-versions/{versionId:guid}/publish")]
|
||||
[EndpointSummary("发布 SaaS 套餐版本")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasOfferingVersionItem> PublishVersion(Guid versionId, CancellationToken cancellationToken) =>
|
||||
catalogService.PublishVersionAsync(Actor(), versionId, cancellationToken);
|
||||
public Task<SaasOfferingVersionItem> PublishVersion(Guid versionId, CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.PublishVersionAsync(Actor(), versionId, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("offering-versions/{versionId:guid}/clone")]
|
||||
[EndpointSummary("克隆 SaaS 套餐版本")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasOfferingVersionItem> CloneVersion(Guid versionId, CancellationToken cancellationToken) =>
|
||||
catalogService.CloneVersionAsync(Actor(), versionId, cancellationToken);
|
||||
public Task<SaasOfferingVersionItem> CloneVersion(Guid versionId, CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.CloneVersionAsync(Actor(), versionId, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("offering-versions/{versionId:guid}/retire")]
|
||||
[EndpointSummary("下架 SaaS 套餐版本")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasCatalogManage)]
|
||||
public Task<SaasOfferingVersionItem> RetireVersion(Guid versionId, CancellationToken cancellationToken) =>
|
||||
catalogService.RetireVersionAsync(Actor(), versionId, cancellationToken);
|
||||
public Task<SaasOfferingVersionItem> RetireVersion(Guid versionId, CancellationToken cancellationToken)
|
||||
{
|
||||
return catalogService.RetireVersionAsync(Actor(), versionId, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("orders")]
|
||||
[EndpointSummary("查询平台 SaaS 订单")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingOrder>> Orders(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetOrdersAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingOrder>> Orders(Guid? tenantId, string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetOrdersAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("payments")]
|
||||
[EndpointSummary("查询平台 SaaS 支付记录")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingPayment>> Payments(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetPaymentsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingPayment>> Payments(Guid? tenantId, string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetPaymentsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("refunds")]
|
||||
[EndpointSummary("查询平台 SaaS 退款记录")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingRefund>> Refunds(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetRefundsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingRefund>> Refunds(Guid? tenantId, string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetRefundsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("invoices")]
|
||||
[EndpointSummary("查询平台 SaaS 发票")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetInvoicesAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(Guid? tenantId, string? status, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetInvoicesAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("usage")]
|
||||
[EndpointSummary("查询租户 SaaS 用量")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<TenantFeatureUsage>> Usage(Guid? tenantId, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetUsageAsync(Actor(), new PlatformBillingAdminQuery(tenantId, null, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<TenantFeatureUsage>> Usage(Guid? tenantId, int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetUsageAsync(Actor(), new PlatformBillingAdminQuery(tenantId, null, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("invoices/reminders")]
|
||||
[EndpointSummary("查询平台 SaaS 账单提醒")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<PlatformBillingInvoiceReminder>> InvoiceReminders(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetInvoiceRemindersAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<PlatformBillingInvoiceReminder>> InvoiceReminders(Guid? tenantId, string? status,
|
||||
int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetInvoiceRemindersAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("subscriptions")]
|
||||
[EndpointSummary("查询租户 SaaS 订阅")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<IReadOnlyCollection<TenantSaasSubscription>> Subscriptions(Guid? tenantId, string? status, int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
billingService.GetSubscriptionsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit), cancellationToken);
|
||||
public Task<IReadOnlyCollection<TenantSaasSubscription>> Subscriptions(Guid? tenantId, string? status,
|
||||
int limit = 100, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return billingService.GetSubscriptionsAsync(Actor(), new PlatformBillingAdminQuery(tenantId, status, limit),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("payments/manual/confirm")]
|
||||
[EndpointSummary("确认平台手工支付")]
|
||||
@@ -120,51 +165,74 @@ public sealed class PlatformSaasController(
|
||||
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> ConfirmManualPayment(
|
||||
ConfirmManualPlatformPaymentDto request,
|
||||
[FromHeader(Name = "Idempotency-Key"), Required] string idempotencyKey,
|
||||
CancellationToken cancellationToken) =>
|
||||
CommandResult(await approvalService.ConfirmManualPaymentAsync(Actor(), request.ToCommand(), idempotencyKey, cancellationToken));
|
||||
[FromHeader(Name = "Idempotency-Key")] [Required]
|
||||
string idempotencyKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return CommandResult(await approvalService.ConfirmManualPaymentAsync(Actor(), request.ToCommand(),
|
||||
idempotencyKey, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("tenant-feature-overrides")]
|
||||
[EndpointSummary("新增或更新租户功能覆盖规则")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantFeatureOverride> UpsertFeatureOverride(UpsertTenantFeatureOverrideDto request, CancellationToken cancellationToken) =>
|
||||
billingService.UpsertFeatureOverrideAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<TenantFeatureOverride> UpsertFeatureOverride(UpsertTenantFeatureOverrideDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.UpsertFeatureOverrideAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("metrics")]
|
||||
[EndpointSummary("查询 SaaS 商业经营指标")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<CommercialMetrics> Metrics(CancellationToken cancellationToken) =>
|
||||
billingService.GetCommercialMetricsAsync(Actor(), cancellationToken);
|
||||
public Task<CommercialMetrics> Metrics(CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.GetCommercialMetricsAsync(Actor(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscriptions/trial")]
|
||||
[EndpointSummary("为已有租户补录试用订阅")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantSaasSubscription> GrantTrial(GrantTenantTrialDto request, CancellationToken cancellationToken) =>
|
||||
billingService.GrantTrialAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<TenantSaasSubscription> GrantTrial(GrantTenantTrialDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.GrantTrialAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscriptions/{subscriptionId:guid}/suspend")]
|
||||
[EndpointSummary("暂停租户 SaaS 订阅")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantSaasSubscription> SuspendSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request, CancellationToken cancellationToken) =>
|
||||
billingService.SuspendSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
public Task<TenantSaasSubscription> SuspendSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.SuspendSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscriptions/{subscriptionId:guid}/resume")]
|
||||
[EndpointSummary("恢复租户 SaaS 订阅")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantSaasSubscription> ResumeSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request, CancellationToken cancellationToken) =>
|
||||
billingService.ResumeSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
public Task<TenantSaasSubscription> ResumeSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.ResumeSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscriptions/{subscriptionId:guid}/cancel")]
|
||||
[EndpointSummary("立即取消租户 SaaS 订阅")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantSaasSubscription> CancelSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request, CancellationToken cancellationToken) =>
|
||||
billingService.CancelSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
public Task<TenantSaasSubscription> CancelSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.CancelSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscriptions/{subscriptionId:guid}/extend")]
|
||||
[EndpointSummary("延长租户 SaaS 订阅账期")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<TenantSaasSubscription> ExtendSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request, CancellationToken cancellationToken) =>
|
||||
billingService.ExtendSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
public Task<TenantSaasSubscription> ExtendSubscription(Guid subscriptionId, ChangePlatformSubscriptionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.ExtendSubscriptionAsync(Actor(), request.ToCommand(subscriptionId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("refunds")]
|
||||
[EndpointSummary("申请平台 SaaS 退款")]
|
||||
@@ -172,31 +240,48 @@ public sealed class PlatformSaasController(
|
||||
[PlatformOperationRisk("high", PlatformApprovalPolicyCodes.FinancialAdjustment)]
|
||||
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<PlatformCommandSubmission>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> RequestRefund(RequestPlatformRefundDto request, CancellationToken cancellationToken) =>
|
||||
CommandResult(await approvalService.SubmitRefundAsync(Actor(), request.ToCommand(), cancellationToken));
|
||||
public async Task<ActionResult<PlatformCommandSubmission>> RequestRefund(RequestPlatformRefundDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return CommandResult(await approvalService.SubmitRefundAsync(Actor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("refunds/{refundId:guid}/approve")]
|
||||
[EndpointSummary("批准平台 SaaS 退款")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<PlatformBillingRefund> ApproveRefund(Guid refundId, ReviewPlatformRefundDto request, CancellationToken cancellationToken) =>
|
||||
billingService.ApproveRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
public Task<PlatformBillingRefund> ApproveRefund(Guid refundId, ReviewPlatformRefundDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.ApproveRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("refunds/{refundId:guid}/reject")]
|
||||
[EndpointSummary("拒绝平台 SaaS 退款")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<PlatformBillingRefund> RejectRefund(Guid refundId, ReviewPlatformRefundDto request, CancellationToken cancellationToken) =>
|
||||
billingService.RejectRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
public Task<PlatformBillingRefund> RejectRefund(Guid refundId, ReviewPlatformRefundDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.RejectRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("refunds/{refundId:guid}/retry")]
|
||||
[EndpointSummary("重试失败的平台 SaaS 退款")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSaasBillingManage)]
|
||||
public Task<PlatformBillingRefund> RetryRefund(Guid refundId, ReviewPlatformRefundDto request, CancellationToken cancellationToken) =>
|
||||
billingService.RetryRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
public Task<PlatformBillingRefund> RetryRefund(Guid refundId, ReviewPlatformRefundDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return billingService.RetryRefundAsync(Actor(), request.ToCommand(refundId), cancellationToken);
|
||||
}
|
||||
|
||||
private SaasCatalogActor Actor() => currentUser.UserId is { } userId
|
||||
? new SaasCatalogActor(userId)
|
||||
: throw new PlatformBillingException("Platform actor was not resolved.", "platform_access_denied");
|
||||
private SaasCatalogActor Actor()
|
||||
{
|
||||
return currentUser.UserId is { } userId
|
||||
? new SaasCatalogActor(userId)
|
||||
: throw new PlatformBillingException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
|
||||
private ActionResult<PlatformCommandSubmission> CommandResult(PlatformCommandSubmission result) =>
|
||||
result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
|
||||
}
|
||||
private ActionResult<PlatformCommandSubmission> CommandResult(PlatformCommandSubmission result)
|
||||
{
|
||||
return result.ExecutionStatus == "pending_approval" ? Accepted(result) : Ok(result);
|
||||
}
|
||||
}
|
||||
@@ -21,36 +21,53 @@ public sealed class PlatformAdminCrmController(
|
||||
[HttpGet("configs")]
|
||||
[EndpointSummary("查询租户 CRM 配置")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformCrmRead)]
|
||||
public Task<PlatformTenantCapabilityList<PlatformCrmConfigItem>> Configs([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
crmService.GetConfigsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<PlatformCrmConfigItem>> Configs(
|
||||
[FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return crmService.GetConfigsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("configs")]
|
||||
[EndpointSummary("新增或更新租户 CRM 配置")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformCrmWrite)]
|
||||
public Task<PlatformCrmConfigItem> UpsertConfig(UpsertPlatformCrmConfigDto request, CancellationToken cancellationToken) =>
|
||||
crmService.UpsertConfigAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<PlatformCrmConfigItem> UpsertConfig(UpsertPlatformCrmConfigDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return crmService.UpsertConfigAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("leads")]
|
||||
[EndpointSummary("查询租户 CRM 线索队列")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformCrmRead)]
|
||||
public Task<PlatformTenantCapabilityList<CrmWebhookQueueItem>> Leads([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
crmService.GetLeadsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<CrmWebhookQueueItem>> Leads([FromQuery] PlatformCapabilityQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return crmService.GetLeadsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("leads/retry")]
|
||||
[EndpointSummary("重试租户 CRM 线索推送")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformCrmWrite)]
|
||||
public Task<CrmWebhookQueueItem> RetryLead(RetryPlatformCrmLeadDto request, CancellationToken cancellationToken) =>
|
||||
crmService.RetryLeadAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<CrmWebhookQueueItem> RetryLead(RetryPlatformCrmLeadDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
return crmService.RetryLeadAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
[EndpointSummary("查询租户 CRM 推送日志")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformCrmRead)]
|
||||
public Task<PlatformTenantCapabilityList<CrmWebhookLog>> Logs([FromQuery] PlatformCrmLogQueryDto query, CancellationToken cancellationToken) =>
|
||||
crmService.GetLogsAsync(Actor(), query.TenantId, query.QueueId, query.Limit, cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<CrmWebhookLog>> Logs([FromQuery] PlatformCrmLogQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return crmService.GetLogsAsync(Actor(), query.TenantId, query.QueueId, query.Limit, cancellationToken);
|
||||
}
|
||||
|
||||
private PlatformCapabilityActor Actor() => currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
private PlatformCapabilityActor Actor()
|
||||
{
|
||||
return currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
@@ -65,54 +82,78 @@ public sealed class PlatformAdminSmsController(
|
||||
[HttpGet("channels")]
|
||||
[EndpointSummary("查询租户短信渠道")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsRead)]
|
||||
public Task<PlatformTenantCapabilityList<PlatformSmsChannelItem>> Channels([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
smsService.GetChannelsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<PlatformSmsChannelItem>> Channels(
|
||||
[FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.GetChannelsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("channels")]
|
||||
[EndpointSummary("新增或更新租户短信渠道")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsWrite)]
|
||||
public Task<PlatformSmsChannelItem> UpsertChannel(UpsertPlatformSmsChannelDto request, CancellationToken cancellationToken) =>
|
||||
smsService.UpsertChannelAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<PlatformSmsChannelItem> UpsertChannel(UpsertPlatformSmsChannelDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.UpsertChannelAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("channels/{id:guid}/disable")]
|
||||
[EndpointSummary("禁用租户短信渠道")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsWrite)]
|
||||
public Task<PlatformSmsChannelItem> DisableChannel(Guid id, CancellationToken cancellationToken) =>
|
||||
smsService.DisableChannelAsync(Actor(), id, cancellationToken);
|
||||
public Task<PlatformSmsChannelItem> DisableChannel(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.DisableChannelAsync(Actor(), id, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("templates")]
|
||||
[EndpointSummary("查询租户短信模板")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsRead)]
|
||||
public Task<PlatformTenantCapabilityList<PlatformSmsTemplateItem>> Templates([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
smsService.GetTemplatesAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<PlatformSmsTemplateItem>> Templates(
|
||||
[FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.GetTemplatesAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("templates")]
|
||||
[EndpointSummary("新增或更新租户短信模板")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsWrite)]
|
||||
public Task<PlatformSmsTemplateItem> UpsertTemplate(UpsertPlatformSmsTemplateDto request, CancellationToken cancellationToken) =>
|
||||
smsService.UpsertTemplateAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
public Task<PlatformSmsTemplateItem> UpsertTemplate(UpsertPlatformSmsTemplateDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.UpsertTemplateAsync(Actor(), request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("templates/{id:guid}/submit-review")]
|
||||
[EndpointSummary("提交租户短信模板审核")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsWrite)]
|
||||
public Task<PlatformSmsTemplateItem> SubmitTemplateReview(Guid id, CancellationToken cancellationToken) =>
|
||||
smsService.SubmitTemplateReviewAsync(Actor(), id, cancellationToken);
|
||||
public Task<PlatformSmsTemplateItem> SubmitTemplateReview(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.SubmitTemplateReviewAsync(Actor(), id, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("templates/{id:guid}/disable")]
|
||||
[EndpointSummary("禁用租户短信模板")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsWrite)]
|
||||
public Task<PlatformSmsTemplateItem> DisableTemplate(Guid id, CancellationToken cancellationToken) =>
|
||||
smsService.DisableTemplateAsync(Actor(), id, cancellationToken);
|
||||
public Task<PlatformSmsTemplateItem> DisableTemplate(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.DisableTemplateAsync(Actor(), id, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
[EndpointSummary("查询租户短信发送日志")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformSmsRead)]
|
||||
public Task<PlatformTenantCapabilityList<SmsSendLog>> Logs([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
smsService.GetLogsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<SmsSendLog>> Logs([FromQuery] PlatformCapabilityQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return smsService.GetLogsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
private PlatformCapabilityActor Actor() => currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
private PlatformCapabilityActor Actor()
|
||||
{
|
||||
return currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
@@ -127,22 +168,34 @@ public sealed class PlatformAdminTenantPaymentSettingsController(
|
||||
[HttpGet("apps")]
|
||||
[EndpointSummary("查询租户支付应用")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<PlatformTenantCapabilityList<TenantExternalProviderItem>> Apps([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
paymentService.GetAppsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<TenantExternalProviderItem>> Apps(
|
||||
[FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentService.GetAppsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPut("apps")]
|
||||
[EndpointSummary("新增或更新租户支付应用")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentWrite)]
|
||||
public Task<TenantExternalProviderItem> UpsertApp(UpsertPlatformTenantPaymentAppDto request, CancellationToken cancellationToken) =>
|
||||
paymentService.UpsertAppAsync(Actor(), request.TenantId, request.ToCommand(), cancellationToken);
|
||||
public Task<TenantExternalProviderItem> UpsertApp(UpsertPlatformTenantPaymentAppDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentService.UpsertAppAsync(Actor(), request.TenantId, request.ToCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("events")]
|
||||
[EndpointSummary("查询租户支付事件")]
|
||||
[Authorize(Policy = BackendPermissions.PlatformPaymentRead)]
|
||||
public Task<PlatformTenantCapabilityList<object>> Events([FromQuery] PlatformCapabilityQueryDto query, CancellationToken cancellationToken) =>
|
||||
paymentService.GetEventsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
public Task<PlatformTenantCapabilityList<object>> Events([FromQuery] PlatformCapabilityQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return paymentService.GetEventsAsync(Actor(), query.ToQuery(), cancellationToken);
|
||||
}
|
||||
|
||||
private PlatformCapabilityActor Actor() => currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
private PlatformCapabilityActor Actor()
|
||||
{
|
||||
return currentUser.UserId is { } userId
|
||||
? new PlatformCapabilityActor(userId)
|
||||
: throw new PlatformCapabilityException("Platform actor was not resolved.", "platform_access_denied");
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,19 @@ public sealed class PlatformTenantLifecycleController(
|
||||
{
|
||||
[HttpGet("archive-preview")]
|
||||
[EndpointSummary("预检租户归档条件")]
|
||||
public async Task<ActionResult<TenantArchivePreview>> Preview(Guid tenantId, CancellationToken cancellationToken) =>
|
||||
Ok(await lifecycleService.PreviewArchiveAsync(tenantId, cancellationToken));
|
||||
public async Task<ActionResult<TenantArchivePreview>> Preview(Guid tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await lifecycleService.PreviewArchiveAsync(tenantId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("exports")]
|
||||
[EndpointSummary("创建租户数据导出")]
|
||||
public async Task<ActionResult<TenantLifecycleOperationItem>> CreateExport(
|
||||
Guid tenantId,
|
||||
CancellationToken cancellationToken) =>
|
||||
Accepted(await lifecycleService.CreateExportAsync(tenantId, ResolveUserId(), cancellationToken));
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Accepted(await lifecycleService.CreateExportAsync(tenantId, ResolveUserId(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("exports/{operationId:guid}")]
|
||||
[EndpointSummary("查询租户数据导出状态")]
|
||||
@@ -43,34 +47,44 @@ public sealed class PlatformTenantLifecycleController(
|
||||
public async Task<ActionResult<ObjectStorageSignedUrl>> DownloadExport(
|
||||
Guid tenantId,
|
||||
Guid operationId,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await lifecycleService.SignExportDownloadAsync(tenantId, operationId, cancellationToken));
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await lifecycleService.SignExportDownloadAsync(tenantId, operationId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("archive")]
|
||||
[EndpointSummary("逻辑归档租户")]
|
||||
public async Task<ActionResult<TenantLifecycleOperationItem>> Archive(
|
||||
Guid tenantId,
|
||||
TenantLifecycleReasonDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await lifecycleService.ArchiveAsync(tenantId, ResolveUserId(), request.Reason, cancellationToken));
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await lifecycleService.ArchiveAsync(tenantId, ResolveUserId(), request.Reason, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("restore")]
|
||||
[EndpointSummary("恢复租户到暂停状态")]
|
||||
public async Task<ActionResult<TenantLifecycleOperationItem>> Restore(
|
||||
Guid tenantId,
|
||||
TenantLifecycleReasonDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await lifecycleService.RestoreAsync(tenantId, ResolveUserId(), request.Reason, cancellationToken));
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await lifecycleService.RestoreAsync(tenantId, ResolveUserId(), request.Reason, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("owner-transfer")]
|
||||
[EndpointSummary("转移租户所有者")]
|
||||
public async Task<ActionResult<TenantLifecycleOperationItem>> TransferOwner(
|
||||
Guid tenantId,
|
||||
TenantOwnerTransferDto request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Ok(await lifecycleService.TransferOwnerAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await lifecycleService.TransferOwnerAsync(
|
||||
tenantId, ResolveUserId(), request.TargetUserId, request.Reason, cancellationToken));
|
||||
}
|
||||
|
||||
private Guid ResolveUserId() =>
|
||||
currentUser.UserId ?? throw new InvalidOperationException("Current platform user was not resolved.");
|
||||
}
|
||||
private Guid ResolveUserId()
|
||||
{
|
||||
return currentUser.UserId ?? throw new InvalidOperationException("Current platform user was not resolved.");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Points;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("学生端-积分")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/points")]
|
||||
public sealed class PointsController(
|
||||
@@ -93,10 +94,8 @@ public sealed class PointsController(
|
||||
private PointActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new PointException("Current point actor was not resolved.", "point_access_denied");
|
||||
}
|
||||
|
||||
return new PointActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Profile;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -108,7 +109,7 @@ public sealed class ProfileController(
|
||||
}
|
||||
|
||||
[HttpPost("check-in")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[EndpointSummary("每日签到")]
|
||||
[ProducesResponseType<CheckInResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CheckInResult>> CheckIn(CancellationToken cancellationToken)
|
||||
@@ -117,7 +118,7 @@ public sealed class ProfileController(
|
||||
}
|
||||
|
||||
[HttpGet("score-events")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[EndpointSummary("查询积分流水")]
|
||||
[ProducesResponseType<ProfileScoreEventList>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ProfileScoreEventList>> ScoreEvents(
|
||||
@@ -146,10 +147,8 @@ public sealed class ProfileController(
|
||||
private ProfileActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ProfileException("Current profile actor was not resolved.", "profile_access_denied");
|
||||
}
|
||||
|
||||
return new ProfileActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Security;
|
||||
@@ -10,7 +11,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("学生端-题目视频")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/questions/videos")]
|
||||
public sealed class QuestionVideosController(
|
||||
@@ -25,7 +26,8 @@ public sealed class QuestionVideosController(
|
||||
[FromQuery] QuestionVideoQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), query.ToQuery(), cancellationToken));
|
||||
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), query.ToQuery(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("batch")]
|
||||
@@ -35,16 +37,15 @@ public sealed class QuestionVideosController(
|
||||
QuestionVideoQueryDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), request.ToQuery(), cancellationToken));
|
||||
return Ok(await videoPlaybackService.GetQuestionVideosAsync(ResolveActor(), request.ToQuery(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private VideoPlaybackActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new VideoPlaybackException("Current video actor was not resolved.", "video_access_denied");
|
||||
}
|
||||
|
||||
return new VideoPlaybackActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-推荐增长")]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.ReferralCommission)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/referral")]
|
||||
public sealed class ReferralController(
|
||||
@@ -202,9 +202,7 @@ public sealed class ReferralController(
|
||||
private ReferralActor ResolveUserActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ReferralException("Current referral actor was not resolved.", "referral_access_denied");
|
||||
}
|
||||
|
||||
return new ReferralActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
@@ -212,29 +210,21 @@ public sealed class ReferralController(
|
||||
private ReferralAdminActor ResolveAdminActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ReferralException("Referral admin actor was not resolved.", "referral_access_denied");
|
||||
}
|
||||
|
||||
return new ReferralAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolveTenantIdAsync(string? tenantCode, CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value;
|
||||
|
||||
var resolvedTenantCode = tenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(resolvedTenantCode))
|
||||
{
|
||||
throw new TenantNotFoundException();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(resolvedTenantCode)) throw new TenantNotFoundException();
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(resolvedTenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,25 +24,21 @@ public sealed class RuntimeController(
|
||||
public async Task<ActionResult<TenantRuntimeBootstrap>> Bootstrap(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!tenantContext.TenantId.HasValue)
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Tenant was not found.",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
var runtime = await frontendConfigService.GetRuntimeAsync(
|
||||
tenantContext.TenantId.Value,
|
||||
cancellationToken);
|
||||
var etag = $"\"{runtime.TenantCode}-{runtime.ConfigVersion}\"";
|
||||
if (Request.Headers.IfNoneMatch.Any(value => string.Equals(value, etag, StringComparison.Ordinal)))
|
||||
{
|
||||
return StatusCode(StatusCodes.Status304NotModified);
|
||||
}
|
||||
|
||||
Response.Headers.ETag = etag;
|
||||
Response.Headers.CacheControl = "public,max-age=60,must-revalidate";
|
||||
return Ok(runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,18 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Scoreline;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("学生端-分数线")]
|
||||
[AllowAnonymous]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/public/scoreline")]
|
||||
public sealed class ScorelineController(
|
||||
@@ -97,16 +97,10 @@ public sealed class ScorelineController(
|
||||
foreach (var (key, value) in Request.Query)
|
||||
{
|
||||
var prefix = DynamicPrefixes.FirstOrDefault(key.StartsWith);
|
||||
if (prefix is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (prefix is null) continue;
|
||||
|
||||
var raw = value.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(raw)) continue;
|
||||
|
||||
filters.Add(new ScorelineDynamicFilter(
|
||||
prefix.TrimEnd('.'),
|
||||
@@ -121,18 +115,12 @@ public sealed class ScorelineController(
|
||||
ScorelineQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentTenant.TenantId.HasValue)
|
||||
{
|
||||
return currentTenant.TenantId.Value;
|
||||
}
|
||||
if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value;
|
||||
|
||||
var tenantCode = query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
throw new TenantNotFoundException();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(tenantCode)) throw new TenantNotFoundException();
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode, cancellationToken);
|
||||
return tenant?.TenantId ?? throw new TenantNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,4 +44,4 @@ public sealed class SecurityDiagnosticsController(
|
||||
currentTenant.TenantId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
@@ -16,7 +17,7 @@ public sealed class TaxonomyController(
|
||||
ITaxonomyService taxonomyService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Practice)]
|
||||
[EndpointSummary("查询租户分类节点")]
|
||||
public Task<IReadOnlyCollection<TaxonomyNodeItem>> List(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -24,7 +25,7 @@ public sealed class TaxonomyController(
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[EndpointSummary("创建租户分类节点")]
|
||||
public Task<TaxonomyNodeItem> Create(
|
||||
@@ -38,4 +39,4 @@ public sealed class TaxonomyController(
|
||||
{
|
||||
return tenantContext.TenantId ?? throw new InvalidOperationException("Tenant was not resolved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.TenantAdmin;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -81,7 +80,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantAdminClassMemberDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertClassMemberAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertClassMemberAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("classes/members/remove")]
|
||||
@@ -92,7 +92,8 @@ public sealed class TenantAdminDirectController(
|
||||
RemoveTenantAdminClassMemberDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.RemoveClassMemberAsync(ResolveActor(), request.ClassMemberId, cancellationToken));
|
||||
return Ok(await tenantAdminService.RemoveClassMemberAsync(ResolveActor(), request.ClassMemberId,
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("students")]
|
||||
@@ -125,7 +126,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpdateTenantAdminStudentStatusDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpdateStudentStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpdateStudentStatusAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("students/import/preview")]
|
||||
@@ -136,7 +138,8 @@ public sealed class TenantAdminDirectController(
|
||||
TenantAdminStudentImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.PreviewStudentImportAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.PreviewStudentImportAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("students/import")]
|
||||
@@ -158,7 +161,8 @@ public sealed class TenantAdminDirectController(
|
||||
TenantAdminBulkAssignClassDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.BulkAssignClassAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.BulkAssignClassAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("students/bulk-status")]
|
||||
@@ -169,14 +173,16 @@ public sealed class TenantAdminDirectController(
|
||||
TenantAdminBulkStatusDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.BulkUpdateStudentStatusAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.BulkUpdateStudentStatusAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("students/supervision/rules")]
|
||||
[Authorize(Policy = BackendPermissions.TenantStudentManage)]
|
||||
[EndpointSummary("查询学习督导规则")]
|
||||
[ProducesResponseType<CatalogList<TenantSupervisionRuleItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantSupervisionRuleItem>>> SupervisionRules(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<CatalogList<TenantSupervisionRuleItem>>> SupervisionRules(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetSupervisionRulesAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
@@ -189,7 +195,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantSupervisionRuleDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertSupervisionRuleAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertSupervisionRuleAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("students/supervision/preview")]
|
||||
@@ -209,7 +216,8 @@ public sealed class TenantAdminDirectController(
|
||||
TenantSupervisionGenerateDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GenerateSupervisionFollowupsAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.GenerateSupervisionFollowupsAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("student-followups/report")]
|
||||
@@ -258,7 +266,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantAdminStudentNoteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertStudentNoteAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertStudentNoteAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("student-followups")]
|
||||
@@ -269,7 +278,8 @@ public sealed class TenantAdminDirectController(
|
||||
[FromQuery] TenantAdminStudentActivityQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetStudentFollowupsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(
|
||||
await tenantAdminService.GetStudentFollowupsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("student-followups")]
|
||||
@@ -280,7 +290,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantAdminStudentFollowupDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertStudentFollowupAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertStudentFollowupAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("members")]
|
||||
@@ -374,7 +385,8 @@ public sealed class TenantAdminDirectController(
|
||||
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
|
||||
[EndpointSummary("查询可用租户主题模板")]
|
||||
[ProducesResponseType<CatalogList<TenantThemeTemplateItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<CatalogList<TenantThemeTemplateItem>>> GetThemeTemplates(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetThemeTemplatesAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
@@ -383,7 +395,8 @@ public sealed class TenantAdminDirectController(
|
||||
[Authorize(Policy = BackendPermissions.TenantSettingsManage)]
|
||||
[EndpointSummary("查询租户当前主题与草稿")]
|
||||
[ProducesResponseType<ContentManagementResult<TenantThemeItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<ContentManagementResult<TenantThemeItem>>> GetTheme(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetThemeAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
@@ -434,7 +447,8 @@ public sealed class TenantAdminDirectController(
|
||||
[Authorize(Policy = BackendPermissions.TenantProviderManage)]
|
||||
[EndpointSummary("查询租户登录 Provider 公开配置")]
|
||||
[ProducesResponseType<CatalogList<TenantIdentityProviderItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<TenantIdentityProviderItem>>> GetAuthProviders(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<CatalogList<TenantIdentityProviderItem>>> GetAuthProviders(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.GetAuthProvidersAsync(ResolveActor(), cancellationToken));
|
||||
}
|
||||
@@ -447,7 +461,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantIdentityProviderDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertAuthProviderAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertAuthProviderAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("badges")]
|
||||
@@ -513,7 +528,8 @@ public sealed class TenantAdminDirectController(
|
||||
UpsertTenantAdminNotificationDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await tenantAdminService.UpsertNotificationAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await tenantAdminService.UpsertNotificationAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("feedbacks")]
|
||||
@@ -549,4 +565,4 @@ public sealed class TenantAdminDirectController(
|
||||
throw new TenantAdminDirectException(exception.Message, "tenant_admin_access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ public sealed class TenantBackofficeController(
|
||||
[ProducesResponseType<BackofficeBootstrap>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<BackofficeBootstrap>> GetBootstrap(CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backofficeService.GetTenantBootstrapAsync(await ResolveActorAsync(cancellationToken), cancellationToken));
|
||||
return Ok(await backofficeService.GetTenantBootstrapAsync(await ResolveActorAsync(cancellationToken),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("roles")]
|
||||
@@ -42,7 +43,8 @@ public sealed class TenantBackofficeController(
|
||||
UpsertBackofficeRoleDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backofficeService.UpsertTenantRoleAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(), cancellationToken));
|
||||
return Ok(await backofficeService.UpsertTenantRoleAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("roles/{roleId:guid}/bindings")]
|
||||
@@ -54,7 +56,8 @@ public sealed class TenantBackofficeController(
|
||||
ReplaceRoleBindingsDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await backofficeService.ReplaceTenantRoleBindingsAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(roleId), cancellationToken));
|
||||
return Ok(await backofficeService.ReplaceTenantRoleBindingsAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(roleId), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("users/{userId:guid}/roles")]
|
||||
@@ -66,7 +69,8 @@ public sealed class TenantBackofficeController(
|
||||
ReplaceUserRolesDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await backofficeService.ReplaceTenantUserRolesAsync(await ResolveActorAsync(cancellationToken), request.ToCommand(userId), cancellationToken);
|
||||
await backofficeService.ReplaceTenantUserRolesAsync(await ResolveActorAsync(cancellationToken),
|
||||
request.ToCommand(userId), cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -74,4 +78,4 @@ public sealed class TenantBackofficeController(
|
||||
{
|
||||
return BackofficeActor.FromTenantAccess(await currentAccessContext.GetAsync(cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,78 +17,122 @@ public sealed class TenantBillingController(
|
||||
{
|
||||
[HttpGet("catalog")]
|
||||
[EndpointSummary("查询租户可购买 SaaS 目录")]
|
||||
public async Task<TenantBillingCatalog> Catalog(CancellationToken cancellationToken) =>
|
||||
await billingService.GetCatalogAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<TenantBillingCatalog> Catalog(CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.GetCatalogAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("quotes")]
|
||||
[EndpointSummary("创建租户账务报价")]
|
||||
public async Task<PlatformBillingQuoteView> Quote(CreatePlatformBillingQuoteDto request, CancellationToken cancellationToken) =>
|
||||
await billingService.CreateQuoteAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
|
||||
public async Task<PlatformBillingQuoteView> Quote(CreatePlatformBillingQuoteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.CreateQuoteAsync(await ActorAsync(cancellationToken), request.ToCommand(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("orders")]
|
||||
[EndpointSummary("创建租户账务订单")]
|
||||
public async Task<PlatformBillingOrderView> CreateOrder(CreatePlatformBillingOrderDto request, CancellationToken cancellationToken) =>
|
||||
await billingService.CreateOrderAsync(await ActorAsync(cancellationToken), request.ToCommand(), cancellationToken);
|
||||
public async Task<PlatformBillingOrderView> CreateOrder(CreatePlatformBillingOrderDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.CreateOrderAsync(await ActorAsync(cancellationToken), request.ToCommand(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("orders/{orderNo}/payments")]
|
||||
[EndpointSummary("创建租户账务订单支付")]
|
||||
public async Task<PlatformBillingPaymentView> CreatePayment(string orderNo, CreatePlatformBillingPaymentDto request, CancellationToken cancellationToken) =>
|
||||
await billingService.CreatePaymentAsync(await ActorAsync(cancellationToken), request.ToCommand(orderNo), cancellationToken);
|
||||
public async Task<PlatformBillingPaymentView> CreatePayment(string orderNo, CreatePlatformBillingPaymentDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.CreatePaymentAsync(await ActorAsync(cancellationToken), request.ToCommand(orderNo),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("orders")]
|
||||
[EndpointSummary("查询租户账务订单")]
|
||||
public async Task<IReadOnlyCollection<PlatformBillingOrderView>> Orders(int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
await billingService.GetOrdersAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformBillingOrderView>> Orders(int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await billingService.GetOrdersAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("orders/{orderNo}/cancel")]
|
||||
[EndpointSummary("取消待支付租户账务订单")]
|
||||
public async Task<PlatformBillingOrderView> CancelOrder(string orderNo, CancellationToken cancellationToken) =>
|
||||
await billingService.CancelOrderAsync(await ActorAsync(cancellationToken), orderNo, cancellationToken);
|
||||
public async Task<PlatformBillingOrderView> CancelOrder(string orderNo, CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.CancelOrderAsync(await ActorAsync(cancellationToken), orderNo, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("orders/{orderNo}")]
|
||||
[EndpointSummary("查询租户账务订单详情")]
|
||||
public async Task<PlatformBillingOrderView> Order(string orderNo, CancellationToken cancellationToken) =>
|
||||
await billingService.GetOrderAsync(await ActorAsync(cancellationToken), orderNo, cancellationToken);
|
||||
public async Task<PlatformBillingOrderView> Order(string orderNo, CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.GetOrderAsync(await ActorAsync(cancellationToken), orderNo, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("subscription")]
|
||||
[EndpointSummary("查询当前租户 SaaS 订阅")]
|
||||
public async Task<TenantSubscriptionView?> Subscription(CancellationToken cancellationToken) =>
|
||||
await billingService.GetSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<TenantSubscriptionView?> Subscription(CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.GetSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscription/change")]
|
||||
[EndpointSummary("变更当前租户 SaaS 订阅")]
|
||||
public async Task<PlatformBillingOrderView> Change(ChangeTenantSubscriptionDto request, CancellationToken cancellationToken) =>
|
||||
await billingService.ChangeSubscriptionAsync(await ActorAsync(cancellationToken), request.ToCommand(), request.IdempotencyKey, cancellationToken);
|
||||
public async Task<PlatformBillingOrderView> Change(ChangeTenantSubscriptionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.ChangeSubscriptionAsync(await ActorAsync(cancellationToken), request.ToCommand(),
|
||||
request.IdempotencyKey, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscription/renew")]
|
||||
[EndpointSummary("续费当前租户 SaaS 订阅")]
|
||||
public async Task<PlatformBillingOrderView> Renew(IdempotentTenantBillingDto request, CancellationToken cancellationToken) =>
|
||||
await billingService.RenewSubscriptionAsync(await ActorAsync(cancellationToken), request.IdempotencyKey, cancellationToken);
|
||||
public async Task<PlatformBillingOrderView> Renew(IdempotentTenantBillingDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.RenewSubscriptionAsync(await ActorAsync(cancellationToken), request.IdempotencyKey,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("subscription/cancel")]
|
||||
[EndpointSummary("取消当前租户 SaaS 订阅")]
|
||||
public async Task<TenantSubscriptionView> Cancel(CancellationToken cancellationToken) =>
|
||||
await billingService.CancelSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<TenantSubscriptionView> Cancel(CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.CancelSubscriptionAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("usage")]
|
||||
[EndpointSummary("查询当前租户功能用量")]
|
||||
public async Task<IReadOnlyCollection<FeatureQuotaSnapshot>> Usage(CancellationToken cancellationToken) =>
|
||||
await billingService.GetUsageAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
public async Task<IReadOnlyCollection<FeatureQuotaSnapshot>> Usage(CancellationToken cancellationToken)
|
||||
{
|
||||
return await billingService.GetUsageAsync(await ActorAsync(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("invoices")]
|
||||
[EndpointSummary("查询当前租户发票")]
|
||||
public async Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
await billingService.GetInvoicesAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformBillingInvoice>> Invoices(int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await billingService.GetInvoicesAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("receivables")]
|
||||
[EndpointSummary("查询当前租户应收账单")]
|
||||
public async Task<IReadOnlyCollection<TenantReceivableView>> Receivables(int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
await billingService.GetReceivablesAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
public async Task<IReadOnlyCollection<TenantReceivableView>> Receivables(int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await billingService.GetReceivablesAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpGet("refunds")]
|
||||
[EndpointSummary("查询当前租户 SaaS 退款")]
|
||||
public async Task<IReadOnlyCollection<PlatformBillingRefund>> Refunds(int limit = 100, CancellationToken cancellationToken = default) =>
|
||||
await billingService.GetRefundsAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
public async Task<IReadOnlyCollection<PlatformBillingRefund>> Refunds(int limit = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await billingService.GetRefundsAsync(await ActorAsync(cancellationToken), limit, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<TenantBillingActor> ActorAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -97,4 +141,4 @@ public sealed class TenantBillingController(
|
||||
? new TenantBillingActor(userId, tenantId)
|
||||
: throw new PlatformBillingException("Tenant billing actor was not resolved.", "tenant_access_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
@@ -11,7 +12,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("租户端-交易运营")]
|
||||
[Authorize(Policy = BackendPermissions.TenantCommerceOperate)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.StudentStore)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/commerce")]
|
||||
public sealed class TenantCommerceController(
|
||||
@@ -504,7 +505,8 @@ public sealed class TenantCommerceController(
|
||||
[Authorize(Policy = TikuPolicies.TenantCommerceOperateAllScope)]
|
||||
[EndpointSummary("查询对账异常汇总")]
|
||||
[ProducesResponseType<TenantCommerceAnomalySummary>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TenantCommerceAnomalySummary>> ReconciliationAnomalies(CancellationToken cancellationToken)
|
||||
public async Task<ActionResult<TenantCommerceAnomalySummary>> ReconciliationAnomalies(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await commerceAdminService.GetAnomalySummaryAsync(
|
||||
ResolveActor(),
|
||||
@@ -570,9 +572,7 @@ public sealed class TenantCommerceController(
|
||||
private CommerceAdminActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new CommerceException("Tenant commerce actor was not resolved.", "tenant_admin_access_denied");
|
||||
}
|
||||
|
||||
return new CommerceAdminActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
@@ -586,4 +586,4 @@ public sealed class TenantCommerceController(
|
||||
{
|
||||
return new TenantPointQuery(query.Status, query.Limit, query.UserId, query.RegionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,9 +306,8 @@ public sealed class TenantContentController(
|
||||
private AssetManagementActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new AssetManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied");
|
||||
}
|
||||
throw new AssetManagementException("Tenant content actor was not resolved.",
|
||||
"tenant_content_access_denied");
|
||||
|
||||
return new AssetManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
@@ -316,10 +315,9 @@ public sealed class TenantContentController(
|
||||
private ContentManagementActor ResolveContentActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied");
|
||||
}
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.",
|
||||
"tenant_content_access_denied");
|
||||
|
||||
return new ContentManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
@@ -24,158 +26,170 @@ public sealed class TenantContentDirectController(
|
||||
[HttpPost("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("创建题目及首个版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.CreateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: true), cancellationToken));
|
||||
return Ok(await directContentService.CreateQuestionAsync(ResolveActor(), request.ToCommand(true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPatch("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("更新题目并可选择创建新版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpdateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: false), cancellationToken));
|
||||
return Ok(await directContentService.UpdateQuestionAsync(ResolveActor(), request.ToCommand(false),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇单元")]
|
||||
[ProducesResponseType<CatalogList<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyUnit>>> GetVocabularyUnits(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetVocabularyUnitsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetVocabularyUnitsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇单元")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyUnit>>> UpsertVocabularyUnit(
|
||||
DirectVocabularyUnitDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertVocabularyUnitAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertVocabularyUnitAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇")]
|
||||
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetVocabularyWordsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetVocabularyWordsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
|
||||
DirectVocabularyWordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertVocabularyWordAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertVocabularyWordAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册科目")]
|
||||
[ProducesResponseType<CatalogList<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookSubject>>> GetHandbookSubjects(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookSubjectsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetHandbookSubjectsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册科目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookSubject>>> UpsertHandbookSubject(
|
||||
DirectHandbookSubjectDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookSubjectAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertHandbookSubjectAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册章节")]
|
||||
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookChaptersAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetHandbookChaptersAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册章节")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
|
||||
DirectHandbookChapterDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookChapterAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertHandbookChapterAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册条目")]
|
||||
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookEntriesAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetHandbookEntriesAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册条目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
|
||||
DirectHandbookEntryDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookEntryAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertHandbookEntryAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线院校")]
|
||||
[ProducesResponseType<CatalogList<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<School>>> GetSchools(
|
||||
@@ -187,7 +201,7 @@ public sealed class TenantContentDirectController(
|
||||
|
||||
[HttpPut("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线院校")]
|
||||
[ProducesResponseType<ContentManagementResult<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<School>>> UpsertSchool(
|
||||
@@ -199,7 +213,7 @@ public sealed class TenantContentDirectController(
|
||||
|
||||
[HttpGet("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线专业")]
|
||||
[ProducesResponseType<CatalogList<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<Major>>> GetMajors(
|
||||
@@ -211,7 +225,7 @@ public sealed class TenantContentDirectController(
|
||||
|
||||
[HttpPut("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线专业")]
|
||||
[ProducesResponseType<ContentManagementResult<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<Major>>> UpsertMajor(
|
||||
@@ -223,80 +237,86 @@ public sealed class TenantContentDirectController(
|
||||
|
||||
[HttpGet("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线字段")]
|
||||
[ProducesResponseType<CatalogList<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineField>>> GetScorelineFields(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineFieldsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetScorelineFieldsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新动态分数线字段")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineField>>> UpsertScorelineField(
|
||||
DirectScorelineFieldDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertScorelineFieldAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertScorelineFieldAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线记录")]
|
||||
[ProducesResponseType<CatalogList<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineRecord>>> GetScorelineRecords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineRecordsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetScorelineRecordsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线记录")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineRecord>>> UpsertScorelineRecord(
|
||||
DirectScorelineRecordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertScorelineRecordAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertScorelineRecordAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/years")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线年份")]
|
||||
[ProducesResponseType<CatalogList<int>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<int>>> GetScorelineYears(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineYearsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(
|
||||
await directContentService.GetScorelineYearsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/trend")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线趋势摘要")]
|
||||
[ProducesResponseType<CatalogList<ScorelineTrendItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineTrendItem>>> GetScorelineTrend(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
return Ok(
|
||||
await directContentService.GetScorelineTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("查询租户视频解析")]
|
||||
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
|
||||
@@ -309,7 +329,7 @@ public sealed class TenantContentDirectController(
|
||||
[HttpPut("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("新增或更新视频解析")]
|
||||
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
|
||||
@@ -322,20 +342,21 @@ public sealed class TenantContentDirectController(
|
||||
[HttpPost("question-videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("绑定题目与解析视频")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
|
||||
DirectQuestionVideoDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.BindQuestionVideoAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.BindQuestionVideoAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询运营内容")]
|
||||
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
|
||||
@@ -343,13 +364,14 @@ public sealed class TenantContentDirectController(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetOperationContentAsync(ResolveActor(), kind, query.ToFilter(), cancellationToken));
|
||||
return Ok(await directContentService.GetOperationContentAsync(ResolveActor(), kind, query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("新增或更新运营内容")]
|
||||
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
|
||||
@@ -357,13 +379,14 @@ public sealed class TenantContentDirectController(
|
||||
DirectOperationContentDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertOperationContentAsync(ResolveActor(), kind, request.ToCommand(), cancellationToken));
|
||||
return Ok(await directContentService.UpsertOperationContentAsync(ResolveActor(), kind, request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/preview/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("预览内容导入数据")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
|
||||
@@ -371,13 +394,14 @@ public sealed class TenantContentDirectController(
|
||||
DirectImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.PreviewImportAsync(ResolveActor(), request.ToCommand(importType, dryRun: true), cancellationToken));
|
||||
return Ok(await directContentService.PreviewImportAsync(ResolveActor(), request.ToCommand(importType, true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("执行或排队内容导入")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<BackgroundJobItem>(StatusCodes.Status202Accepted)]
|
||||
@@ -387,14 +411,14 @@ public sealed class TenantContentDirectController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var actor = ResolveActor();
|
||||
var command = request.ToCommand(importType, dryRun: false);
|
||||
var command = request.ToCommand(importType, false);
|
||||
if (request.Async == true || command.Items.Count > 100)
|
||||
{
|
||||
var job = await backgroundJobService.EnqueueAsync(
|
||||
new CreateBackgroundJobCommand(
|
||||
actor.TenantId,
|
||||
"content_import",
|
||||
System.Text.Json.JsonSerializer.SerializeToElement(new
|
||||
JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
createdBy = actor.UserId,
|
||||
importType = command.ImportType,
|
||||
@@ -467,10 +491,9 @@ public sealed class TenantContentDirectController(
|
||||
private DirectContentActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied");
|
||||
}
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.",
|
||||
"tenant_content_access_denied");
|
||||
|
||||
return new DirectContentActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,4 +52,4 @@ public sealed class TenantFrontendConfigController(
|
||||
"tenant_not_resolved",
|
||||
"Tenant was not resolved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,7 @@ public sealed class TenantOnboardingController(
|
||||
{
|
||||
var access = await accessContext.GetAsync(cancellationToken);
|
||||
if (access.TenantId is not { } tenantId || !access.IsCurrentTenantMember)
|
||||
{
|
||||
throw new TenantExternalProviderException("Tenant onboarding access is denied.", "tenant_access_denied");
|
||||
}
|
||||
return await onboardingService.GetStatusAsync(tenantId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class TenantPublicController(
|
||||
? null
|
||||
: await tenantDirectory.FindByHostAsync(host, cancellationToken);
|
||||
|
||||
TenantLookupResult? tenant = directoryEntry is null
|
||||
var tenant = directoryEntry is null
|
||||
? null
|
||||
: new TenantLookupResult(
|
||||
directoryEntry.TenantId,
|
||||
@@ -49,14 +49,12 @@ public sealed class TenantPublicController(
|
||||
directoryEntry.Host);
|
||||
|
||||
if (tenant is null)
|
||||
{
|
||||
return NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Tenant was not found.",
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Detail = "No active tenant matches the supplied host or tenantCode."
|
||||
});
|
||||
}
|
||||
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.Id,
|
||||
@@ -115,10 +113,7 @@ public sealed class TenantPublicController(
|
||||
|
||||
private static string? NormalizeHost(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
|
||||
var host = value.Trim().ToLowerInvariant();
|
||||
return host.Split(':', 2)[0];
|
||||
@@ -131,4 +126,4 @@ public sealed class TenantPublicController(
|
||||
TenantStatus Status,
|
||||
TenantMode Mode,
|
||||
string? Host);
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,14 @@ public sealed class TenantsController(
|
||||
[EndpointDescription("返回当前请求租户及当前用户在该租户内的成员角色。")]
|
||||
public async Task<ActionResult<CurrentTenantResponse>> GetCurrent(CancellationToken cancellationToken)
|
||||
{
|
||||
if (currentUser.UserId is null || currentTenant.TenantId is null)
|
||||
{
|
||||
throw new TenantAccessDeniedException();
|
||||
}
|
||||
if (currentUser.UserId is null || currentTenant.TenantId is null) throw new TenantAccessDeniedException();
|
||||
|
||||
var membership = await identityQueries.GetTenantMembershipAsync(
|
||||
currentUser.UserId.Value,
|
||||
currentTenant.TenantId.Value,
|
||||
cancellationToken);
|
||||
|
||||
if (membership is null)
|
||||
{
|
||||
throw new TenantAccessDeniedException();
|
||||
}
|
||||
if (membership is null) throw new TenantAccessDeniedException();
|
||||
|
||||
return Ok(new CurrentTenantResponse(
|
||||
membership.TenantId,
|
||||
@@ -45,7 +39,7 @@ public sealed class TenantsController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求租户和当前用户在该租户内的权限摘要。
|
||||
/// 当前请求租户和当前用户在该租户内的权限摘要。
|
||||
/// </summary>
|
||||
/// <param name="TenantId">租户 ID。</param>
|
||||
/// <param name="TenantName">租户名称。</param>
|
||||
@@ -57,4 +51,4 @@ public sealed record CurrentTenantResponse(
|
||||
string TenantName,
|
||||
string TenantSlug,
|
||||
TenantStatus Status,
|
||||
TenantRole Role);
|
||||
TenantRole Role);
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Security;
|
||||
@@ -10,7 +11,7 @@ namespace Tiku.Api.Controllers;
|
||||
[ApiController]
|
||||
[Tags("学生端-视频")]
|
||||
[Authorize(Policy = TikuPolicies.CurrentTenantMember)]
|
||||
[Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[Produces("application/json")]
|
||||
[Route("api/student/videos")]
|
||||
public sealed class VideosController(
|
||||
@@ -45,16 +46,15 @@ public sealed class VideosController(
|
||||
VideoProgressDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await videoPlaybackService.ReportProgressAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
return Ok(
|
||||
await videoPlaybackService.ReportProgressAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
private VideoPlaybackActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new VideoPlaybackException("Current video actor was not resolved.", "video_access_denied");
|
||||
}
|
||||
|
||||
return new VideoPlaybackActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using Serilog;
|
||||
using Serilog.AspNetCore;
|
||||
using Serilog.Events;
|
||||
@@ -33,13 +34,10 @@ public static class SerilogRequestLogging
|
||||
private static void SetClaim(
|
||||
IDiagnosticContext diagnosticContext,
|
||||
string propertyName,
|
||||
System.Security.Claims.ClaimsPrincipal principal,
|
||||
ClaimsPrincipal principal,
|
||||
string claimType)
|
||||
{
|
||||
var value = principal.FindFirst(claimType)?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
diagnosticContext.Set(propertyName, value);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(value)) diagnosticContext.Set(propertyName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Security;
|
||||
@@ -11,7 +12,7 @@ namespace Tiku.Api.Middleware;
|
||||
public sealed class AuthRateLimitPartitionMiddleware(
|
||||
RequestDelegate next,
|
||||
IRedisSecurityStore redisSecurityStore,
|
||||
Microsoft.Extensions.Options.IOptions<AuthRateLimitOptions> options)
|
||||
IOptions<AuthRateLimitOptions> options)
|
||||
{
|
||||
public AuthRateLimitPartitionMiddleware(RequestDelegate next)
|
||||
: this(
|
||||
@@ -40,10 +41,7 @@ public sealed class AuthRateLimitPartitionMiddleware(
|
||||
if (redisSecurityStore.IsConfigured)
|
||||
{
|
||||
await ConsumeDistributedLimitAsync(context, policy!);
|
||||
if (context.Response.HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (context.Response.HasStarted) return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +85,8 @@ public sealed class AuthRateLimitPartitionMiddleware(
|
||||
if (!result.Allowed)
|
||||
{
|
||||
if (result.RetryAfter is { } retryAfter)
|
||||
{
|
||||
context.Response.Headers.RetryAfter = Math.Max(1, (int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
|
||||
}
|
||||
context.Response.Headers.RetryAfter =
|
||||
Math.Max(1, (int)Math.Ceiling(retryAfter.TotalSeconds)).ToString();
|
||||
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
@@ -103,7 +100,7 @@ public sealed class AuthRateLimitPartitionMiddleware(
|
||||
|
||||
private static async Task CaptureAccountHashAsync(HttpContext context, string propertyName)
|
||||
{
|
||||
context.Request.EnableBuffering(bufferThreshold: 4096, bufferLimit: 16_384);
|
||||
context.Request.EnableBuffering(4096, 16_384);
|
||||
try
|
||||
{
|
||||
using var document = await JsonDocument.ParseAsync(
|
||||
@@ -113,9 +110,7 @@ public sealed class AuthRateLimitPartitionMiddleware(
|
||||
(propertyName == "identifier" ? TryGetStringProperty(document.RootElement, "phone") : null);
|
||||
if (captured is { } value &&
|
||||
!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
context.Items[AuthRateLimitPartitionKey.AccountHashItemKey] = Hash(value.Trim());
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
@@ -127,28 +122,18 @@ public sealed class AuthRateLimitPartitionMiddleware(
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (context.Request.Body.CanSeek)
|
||||
{
|
||||
context.Request.Body.Position = 0;
|
||||
}
|
||||
if (context.Request.Body.CanSeek) context.Request.Body.Position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryGetStringProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (element.ValueKind != JsonValueKind.Object) return null;
|
||||
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase) &&
|
||||
property.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return property.Value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -174,4 +159,4 @@ public static class AuthRateLimitPartitionKey
|
||||
: "unknown-account";
|
||||
return $"{policyName}:{ipAddress}:{accountHash}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Options;
|
||||
|
||||
@@ -32,20 +33,21 @@ public sealed class BrowserCsrfMiddleware(
|
||||
await next(context);
|
||||
}
|
||||
|
||||
private static bool IsUnsafe(string method) =>
|
||||
!HttpMethods.IsGet(method) && !HttpMethods.IsHead(method) && !HttpMethods.IsOptions(method);
|
||||
private static bool IsUnsafe(string method)
|
||||
{
|
||||
return !HttpMethods.IsGet(method) && !HttpMethods.IsHead(method) && !HttpMethods.IsOptions(method);
|
||||
}
|
||||
|
||||
private static bool IsBrowserCookieRequest(HttpRequest request) =>
|
||||
request.Cookies.ContainsKey(BrowserAuthOptions.AccessCookie) ||
|
||||
request.Cookies.ContainsKey(BrowserAuthOptions.RefreshCookie);
|
||||
private static bool IsBrowserCookieRequest(HttpRequest request)
|
||||
{
|
||||
return request.Cookies.ContainsKey(BrowserAuthOptions.AccessCookie) ||
|
||||
request.Cookies.ContainsKey(BrowserAuthOptions.RefreshCookie);
|
||||
}
|
||||
|
||||
private bool IsTrustedOrigin(HttpRequest request)
|
||||
{
|
||||
var origin = request.Headers.Origin.ToString().Trim().TrimEnd('/');
|
||||
if (string.IsNullOrWhiteSpace(origin) || !Uri.TryCreate(origin, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(origin) || !Uri.TryCreate(origin, UriKind.Absolute, out var uri)) return false;
|
||||
|
||||
var sameOrigin = string.Equals(uri.Scheme, request.Scheme, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(uri.Authority, request.Host.Value, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -57,12 +59,9 @@ public sealed class BrowserCsrfMiddleware(
|
||||
{
|
||||
var cookie = request.Cookies[BrowserAuthOptions.CsrfCookie];
|
||||
var header = request.Headers[BrowserAuthOptions.CsrfHeader].ToString();
|
||||
if (string.IsNullOrWhiteSpace(cookie) || string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var left = System.Text.Encoding.UTF8.GetBytes(cookie);
|
||||
var right = System.Text.Encoding.UTF8.GetBytes(header);
|
||||
if (string.IsNullOrWhiteSpace(cookie) || string.IsNullOrWhiteSpace(header)) return false;
|
||||
var left = Encoding.UTF8.GetBytes(cookie);
|
||||
var right = Encoding.UTF8.GetBytes(header);
|
||||
return left.Length == right.Length && CryptographicOperations.FixedTimeEquals(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,4 @@ public sealed class CurrentPrincipalMiddleware(RequestDelegate next)
|
||||
currentUser.Load(context.User);
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,4 @@ public sealed class DatabaseRequestMetricsMiddleware(RequestDelegate next)
|
||||
using var metrics = DatabaseRequestMetrics.Begin();
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,21 @@ using Tiku.Api.Controllers;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Backoffice;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Points;
|
||||
using Tiku.Application.Profile;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.PlatformAdmin;
|
||||
using Tiku.Application.PlatformBilling;
|
||||
using Tiku.Application.Points;
|
||||
using Tiku.Application.Profile;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Scoreline;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Application.TenantAdmin;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Application.Learning;
|
||||
using Tiku.Application.TenantAdmin;
|
||||
|
||||
namespace Tiku.Api.Middleware;
|
||||
|
||||
@@ -39,13 +39,16 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
var status = approvalException.Code switch
|
||||
{
|
||||
"approval_request_not_found" or "approval_policy_not_found" => StatusCodes.Status404NotFound,
|
||||
"platform_access_denied" or "approval_business_permission_required" or "approval_cancel_denied" => StatusCodes.Status403Forbidden,
|
||||
"approval_request_not_pending" or "approval_request_expired" or "approval_maker_checker_required" or "idempotency_conflict" => StatusCodes.Status409Conflict,
|
||||
"platform_access_denied" or "approval_business_permission_required" or "approval_cancel_denied" =>
|
||||
StatusCodes.Status403Forbidden,
|
||||
"approval_request_not_pending" or "approval_request_expired" or "approval_maker_checker_required"
|
||||
or "idempotency_conflict" => StatusCodes.Status409Conflict,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
await WriteProblemAsync(context, approvalException.Message, status, approvalException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is AuthorizationSecurityUnavailableException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
@@ -67,7 +70,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
var status = ownerActivationException.Code is "owner_activation_consumed"
|
||||
? StatusCodes.Status409Conflict
|
||||
: StatusCodes.Status400BadRequest;
|
||||
await WriteProblemAsync(context, ownerActivationException.Message, status, ownerActivationException.Code);
|
||||
await WriteProblemAsync(context, ownerActivationException.Message, status,
|
||||
ownerActivationException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,7 +92,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
"tenant_not_found" => StatusCodes.Status404NotFound,
|
||||
"tenant_export_not_ready" => StatusCodes.Status409Conflict,
|
||||
"tenant_archive_blocked" or "tenant_not_archived" or
|
||||
"tenant_owner_target_not_active_member" or "tenant_owner_unchanged" => StatusCodes.Status409Conflict,
|
||||
"tenant_owner_target_not_active_member"
|
||||
or "tenant_owner_unchanged" => StatusCodes.Status409Conflict,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
await WriteProblemAsync(context, lifecycleException.Message, status, lifecycleException.Code);
|
||||
@@ -97,7 +102,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
|
||||
if (exception is BrowserOriginException)
|
||||
{
|
||||
await WriteProblemAsync(context, exception.Message, StatusCodes.Status403Forbidden, "browser_origin_rejected");
|
||||
await WriteProblemAsync(context, exception.Message, StatusCodes.Status403Forbidden,
|
||||
"browser_origin_rejected");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -573,7 +579,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
return code switch
|
||||
{
|
||||
"tenant_admin_access_denied" => StatusCodes.Status403Forbidden,
|
||||
"class_not_found" or "class_member_not_found" or "student_not_found" or "user_not_found" => StatusCodes.Status404NotFound,
|
||||
"class_not_found" or "class_member_not_found" or "student_not_found" or "user_not_found" => StatusCodes
|
||||
.Status404NotFound,
|
||||
"tenant_member_not_found" => StatusCodes.Status404NotFound,
|
||||
_ when code.EndsWith("_not_found", StringComparison.Ordinal) => StatusCodes.Status404NotFound,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
@@ -588,7 +595,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
"tenant_admin_access_denied" => StatusCodes.Status403Forbidden,
|
||||
"order_not_found" or "svip_plan_not_found" or "region_not_found" or "activation_code_not_found" or
|
||||
"coupon_not_found" or "coupon_redemption_not_found" => StatusCodes.Status404NotFound,
|
||||
"payment_provider_not_configured" or "payment_secret_not_configured" => StatusCodes.Status503ServiceUnavailable,
|
||||
"payment_provider_not_configured" or "payment_secret_not_configured" => StatusCodes
|
||||
.Status503ServiceUnavailable,
|
||||
"order_status_invalid" or "activation_code_used" or "payment_amount_mismatch" or
|
||||
"coupon_usage_limit_reached" or "coupon_redemption_status_invalid" => StatusCodes.Status409Conflict,
|
||||
_ when code.EndsWith("_not_found", StringComparison.Ordinal) => StatusCodes.Status404NotFound,
|
||||
@@ -600,7 +608,8 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
"platform_access_denied" or "tenant_access_denied" or "capability_not_available" => StatusCodes.Status403Forbidden,
|
||||
"platform_access_denied" or "tenant_access_denied" or "capability_not_available" => StatusCodes
|
||||
.Status403Forbidden,
|
||||
_ when code.EndsWith("_not_found", StringComparison.Ordinal) => StatusCodes.Status404NotFound,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
@@ -684,4 +693,4 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
@@ -41,18 +41,14 @@ public sealed class TenantResolutionMiddleware(RequestDelegate next)
|
||||
var tenantCode = context.Request.Headers["x-tenant-code"].FirstOrDefault()
|
||||
?? context.Request.Query["tenantCode"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), context.RequestAborted);
|
||||
}
|
||||
}
|
||||
|
||||
if (tenant is not null)
|
||||
{
|
||||
tenantInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
tenant.Host is null ? TenantResolutionSource.TenantCode : TenantResolutionSource.Host);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
@@ -71,4 +67,4 @@ public sealed class TenantResolutionMiddleware(RequestDelegate next)
|
||||
{
|
||||
return prefixes.Any(prefix => path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,7 @@ internal sealed class BearerSecuritySchemeTransformer(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync();
|
||||
if (authenticationSchemes.All(scheme => scheme.Name != JwtBearerDefaults.AuthenticationScheme))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (authenticationSchemes.All(scheme => scheme.Name != JwtBearerDefaults.AuthenticationScheme)) return;
|
||||
|
||||
document.Components ??= new OpenApiComponents();
|
||||
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
||||
@@ -30,4 +27,4 @@ internal sealed class BearerSecuritySchemeTransformer(
|
||||
Description = "输入 JWT access token。不要带 Bearer 前缀,Scalar 会自动补。"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.OpenApi;
|
||||
using Microsoft.OpenApi;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.OpenApi;
|
||||
|
||||
@@ -24,29 +25,22 @@ internal sealed class PlatformOperationMetadataTransformer : IOpenApiOperationTr
|
||||
var authorization = context.Description.ActionDescriptor.EndpointMetadata
|
||||
.OfType<EndpointAuthorizationMetadata>()
|
||||
.LastOrDefault();
|
||||
if (authorization?.Realm != "platform")
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
if (authorization?.Realm != "platform") return Task.CompletedTask;
|
||||
|
||||
if (authorization.Permission is not null)
|
||||
{
|
||||
operation.AddExtension("x-tiku-required-permission",
|
||||
new JsonNodeExtension(JsonValue.Create(authorization.Permission)));
|
||||
}
|
||||
|
||||
var declaredRisk = context.Description.ActionDescriptor.EndpointMetadata
|
||||
.OfType<PlatformOperationRiskAttribute>()
|
||||
.LastOrDefault();
|
||||
var riskLevel = declaredRisk?.RiskLevel ??
|
||||
(authorization.Operation == Tiku.Application.Security.CapabilityOperation.Read ? "low" : "medium");
|
||||
(authorization.Operation == CapabilityOperation.Read ? "low" : "medium");
|
||||
operation.AddExtension("x-tiku-risk-level", new JsonNodeExtension(JsonValue.Create(riskLevel)));
|
||||
if (declaredRisk?.ApprovalPolicyCode is not null)
|
||||
{
|
||||
operation.AddExtension("x-tiku-approval-policy-code",
|
||||
new JsonNodeExtension(JsonValue.Create(declaredRisk.ApprovalPolicyCode)));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tiku.Api.Options;
|
||||
|
||||
public sealed class ApiRateLimitOptions
|
||||
@@ -6,12 +8,9 @@ public sealed class ApiRateLimitOptions
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Range(1, 100_000)]
|
||||
public int PermitLimit { get; set; } = 600;
|
||||
[Range(1, 100_000)] public int PermitLimit { get; set; } = 600;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Range(1, 86_400)]
|
||||
public int WindowSeconds { get; set; } = 60;
|
||||
[Range(1, 86_400)] public int WindowSeconds { get; set; } = 60;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Range(0, 10_000)]
|
||||
public int QueueLimit { get; set; }
|
||||
}
|
||||
[Range(0, 10_000)] public int QueueLimit { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user