更新nuget&&添加swagger
This commit is contained in:
@@ -79,6 +79,38 @@ public sealed class OperationsController(
|
|||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
Ok(await healthService.CheckAsync(cancellationToken));
|
Ok(await healthService.CheckAsync(cancellationToken));
|
||||||
|
|
||||||
|
[HttpGet("swagger")]
|
||||||
|
public async Task<ActionResult<SwaggerDocumentationSettings>> GetSwaggerSettings(
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Ok(new SwaggerDocumentationSettings(await IsSwaggerEnabledAsync(cancellationToken)));
|
||||||
|
|
||||||
|
[HttpPut("swagger")]
|
||||||
|
public async Task<ActionResult<SwaggerDocumentationSettings>> UpdateSwaggerSettings(
|
||||||
|
UpdateSwaggerDocumentationSettings request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var setting = await db.SystemFeatureSettings.SingleOrDefaultAsync(
|
||||||
|
x => x.Key == SystemFeatureKeys.SwaggerDocumentation,
|
||||||
|
cancellationToken);
|
||||||
|
if (setting is null)
|
||||||
|
{
|
||||||
|
setting = new SystemFeatureSetting
|
||||||
|
{
|
||||||
|
Key = SystemFeatureKeys.SwaggerDocumentation,
|
||||||
|
IsEnabled = request.IsEnabled
|
||||||
|
};
|
||||||
|
db.SystemFeatureSettings.Add(setting);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setting.IsEnabled = request.IsEnabled;
|
||||||
|
setting.UpdatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
return Ok(new SwaggerDocumentationSettings(setting.IsEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("audit-logs")]
|
[HttpGet("audit-logs")]
|
||||||
public async Task<ActionResult<PagedResult<AuditLogItem>>> GetAuditLogs(
|
public async Task<ActionResult<PagedResult<AuditLogItem>>> GetAuditLogs(
|
||||||
[FromQuery] int page = 1,
|
[FromQuery] int page = 1,
|
||||||
@@ -523,6 +555,12 @@ public sealed class OperationsController(
|
|||||||
x.CreatedAt >= from,
|
x.CreatedAt >= from,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
private async Task<bool> IsSwaggerEnabledAsync(CancellationToken cancellationToken) =>
|
||||||
|
await db.SystemFeatureSettings.AsNoTracking()
|
||||||
|
.Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
|
||||||
|
.Select(x => (bool?)x.IsEnabled)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken) ?? false;
|
||||||
|
|
||||||
private ActionResult? ValidatePaging(int page, int pageSize)
|
private ActionResult? ValidatePaging(int page, int pageSize)
|
||||||
{
|
{
|
||||||
if (page is < 1 or > 100000 || pageSize is < 1 or > 100)
|
if (page is < 1 or > 100000 || pageSize is < 1 or > 100)
|
||||||
@@ -604,3 +642,7 @@ public sealed record OperationsSummary(
|
|||||||
public sealed record CreateBackupRequest([MaxLength(200)] string? Note);
|
public sealed record CreateBackupRequest([MaxLength(200)] string? Note);
|
||||||
|
|
||||||
public sealed record RestoreDrillRequest([Required] string Confirmation);
|
public sealed record RestoreDrillRequest([Required] string Confirmation);
|
||||||
|
|
||||||
|
public sealed record SwaggerDocumentationSettings(bool IsEnabled);
|
||||||
|
|
||||||
|
public sealed record UpdateSwaggerDocumentationSettings(bool IsEnabled);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Jiaowu.Api.Domain.Common;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Domain.System;
|
||||||
|
|
||||||
|
public static class SystemFeatureKeys
|
||||||
|
{
|
||||||
|
public const string SwaggerDocumentation = "SwaggerDocumentation";
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SystemFeatureSetting : EntityBase
|
||||||
|
{
|
||||||
|
public required string Key { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -137,6 +137,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
Set<BackgroundJobOutboxMessage>();
|
Set<BackgroundJobOutboxMessage>();
|
||||||
public DbSet<AppUpdateRelease> AppUpdateReleases =>
|
public DbSet<AppUpdateRelease> AppUpdateReleases =>
|
||||||
Set<AppUpdateRelease>();
|
Set<AppUpdateRelease>();
|
||||||
|
public DbSet<SystemFeatureSetting> SystemFeatureSettings =>
|
||||||
|
Set<SystemFeatureSetting>();
|
||||||
public DbSet<RefreshSession> RefreshSessions => Set<RefreshSession>();
|
public DbSet<RefreshSession> RefreshSessions => Set<RefreshSession>();
|
||||||
|
|
||||||
protected override void ConfigureConventions(
|
protected override void ConfigureConventions(
|
||||||
@@ -1451,6 +1453,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
entity.HasIndex(x => x.CreatedAt);
|
entity.HasIndex(x => x.CreatedAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Entity<SystemFeatureSetting>(entity =>
|
||||||
|
{
|
||||||
|
entity.Property(x => x.Key).HasMaxLength(100);
|
||||||
|
entity.HasIndex(x => x.Key).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
builder.Entity<OfficialDocument>(entity =>
|
builder.Entity<OfficialDocument>(entity =>
|
||||||
{
|
{
|
||||||
entity.Property(x => x.DocumentNumber).HasMaxLength(50);
|
entity.Property(x => x.DocumentNumber).HasMaxLength(50);
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
"20260809_47_course_grade_distribution";
|
"20260809_47_course_grade_distribution";
|
||||||
private const string TeachingTaskGradeAnalyticsMigration =
|
private const string TeachingTaskGradeAnalyticsMigration =
|
||||||
"20260809_48_teaching_task_grade_analytics";
|
"20260809_48_teaching_task_grade_analytics";
|
||||||
|
private const string SwaggerDocumentationSettingMigration =
|
||||||
|
"20260809_49_swagger_documentation_setting";
|
||||||
|
|
||||||
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
public async Task MigrateAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -669,6 +671,14 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
TeachingTaskGradeAnalyticsMigration,
|
TeachingTaskGradeAnalyticsMigration,
|
||||||
TeachingTaskGradeAnalyticsStatements,
|
TeachingTaskGradeAnalyticsStatements,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
var swaggerSettingsExist = await db.Database
|
||||||
|
.SqlQueryRaw<int>(
|
||||||
|
"SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'SystemFeatureSettings'")
|
||||||
|
.AnyAsync(value => value > 0, cancellationToken);
|
||||||
|
await ApplyMigrationAsync(
|
||||||
|
SwaggerDocumentationSettingMigration,
|
||||||
|
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyMigrationAsync(
|
private async Task ApplyMigrationAsync(
|
||||||
@@ -2913,4 +2923,21 @@ public sealed class DevelopmentSqliteMigrator(
|
|||||||
ADD COLUMN "Kind" INTEGER NOT NULL DEFAULT 1;
|
ADD COLUMN "Kind" INTEGER NOT NULL DEFAULT 1;
|
||||||
"""
|
"""
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly string[] SwaggerDocumentationSettingStatements =
|
||||||
|
[
|
||||||
|
"""
|
||||||
|
CREATE TABLE "SystemFeatureSettings" (
|
||||||
|
"Id" TEXT NOT NULL CONSTRAINT "PK_SystemFeatureSettings" PRIMARY KEY,
|
||||||
|
"Key" TEXT NOT NULL,
|
||||||
|
"IsEnabled" INTEGER NOT NULL,
|
||||||
|
"CreatedAt" TEXT NOT NULL,
|
||||||
|
"UpdatedAt" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
CREATE UNIQUE INDEX "IX_SystemFeatureSettings_Key"
|
||||||
|
ON "SystemFeatureSettings" ("Key");
|
||||||
|
"""
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+6595
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SwaggerDocumentationSetting : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SystemFeatureSettings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SystemFeatureSettings", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SystemFeatureSettings_Key",
|
||||||
|
table: "SystemFeatureSettings",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SystemFeatureSettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -4767,6 +4767,34 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.ToTable("BackgroundJobOutboxMessages");
|
b.ToTable("BackgroundJobOutboxMessages");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Jiaowu.Api.Domain.System.SystemFeatureSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SystemFeatureSettings");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<Import Project="..\..\versions.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Version>2.3.2-beta.1</Version>
|
<Version>$(JiaowuBackendVersion)</Version>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
|
<SpaRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../web'))</SpaRoot>
|
||||||
@@ -18,15 +20,26 @@
|
|||||||
Include="..\..\.env.example"
|
Include="..\..\.env.example"
|
||||||
Link=".env.example"
|
Link=".env.example"
|
||||||
CopyToPublishDirectory="PreserveNewest" />
|
CopyToPublishDirectory="PreserveNewest" />
|
||||||
|
<Content
|
||||||
|
Include="..\..\versions.props"
|
||||||
|
Link="versions.props"
|
||||||
|
CopyToOutputDirectory="PreserveNewest"
|
||||||
|
CopyToPublishDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
<AssemblyMetadata
|
||||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.1.1" />
|
Include="SwaggerDocumentVersion"
|
||||||
|
Value="$(JiaowuSwaggerVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ClosedXML" Version="0.105.1" />
|
||||||
|
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.1.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.8.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.10" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -40,11 +53,11 @@
|
|||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.8.0" />
|
<PackageReference Include="QRCoder" Version="1.8.0" />
|
||||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
|
<PackageReference Include="RabbitMQ.Client" Version="7.2.2" />
|
||||||
<PackageReference Include="SkiaSharp" Version="3.119.2" />
|
<PackageReference Include="SkiaSharp" Version="4.151.1" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.2" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.151.1" />
|
||||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.4" />
|
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target
|
<Target
|
||||||
|
|||||||
+43
-15
@@ -1,6 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Domain.System;
|
||||||
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
using Jiaowu.Api.Infrastructure.BackgroundJobs;
|
||||||
using Jiaowu.Api.Infrastructure.Grades;
|
using Jiaowu.Api.Infrastructure.Grades;
|
||||||
using Jiaowu.Api.Infrastructure.Configuration;
|
using Jiaowu.Api.Infrastructure.Configuration;
|
||||||
@@ -23,10 +24,11 @@ using Microsoft.Data.Sqlite;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi;
|
||||||
using OpenTelemetry.Metrics;
|
using OpenTelemetry.Metrics;
|
||||||
using OpenTelemetry.Resources;
|
using OpenTelemetry.Resources;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
|
|
||||||
EnvironmentFile.Load();
|
EnvironmentFile.Load();
|
||||||
@@ -62,6 +64,13 @@ if (confirmProductionDemoData && !seedDemoData)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
var swaggerDocumentVersion = typeof(Program).Assembly
|
||||||
|
.GetCustomAttributes(
|
||||||
|
typeof(System.Reflection.AssemblyMetadataAttribute),
|
||||||
|
inherit: false)
|
||||||
|
.OfType<System.Reflection.AssemblyMetadataAttribute>()
|
||||||
|
.SingleOrDefault(x => x.Key == "SwaggerDocumentVersion")?.Value
|
||||||
|
?? "v1";
|
||||||
|
|
||||||
if (seedDemoData && builder.Environment.IsDevelopment())
|
if (seedDemoData && builder.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -608,10 +617,10 @@ builder.Services.AddControllers()
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(options =>
|
builder.Services.AddSwaggerGen(options =>
|
||||||
{
|
{
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
options.SwaggerDoc(swaggerDocumentVersion, new OpenApiInfo
|
||||||
{
|
{
|
||||||
Title = "大学教务管理系统 API",
|
Title = "大学教务管理系统 API",
|
||||||
Version = "v1"
|
Version = swaggerDocumentVersion
|
||||||
});
|
});
|
||||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
{
|
{
|
||||||
@@ -621,17 +630,10 @@ builder.Services.AddSwaggerGen(options =>
|
|||||||
BearerFormat = "JWT",
|
BearerFormat = "JWT",
|
||||||
In = ParameterLocation.Header
|
In = ParameterLocation.Header
|
||||||
});
|
});
|
||||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
options.AddSecurityRequirement(_ => new OpenApiSecurityRequirement
|
||||||
{
|
{
|
||||||
[
|
[
|
||||||
new OpenApiSecurityScheme
|
new OpenApiSecuritySchemeReference("Bearer", null, null)
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
] = []
|
] = []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -640,11 +642,37 @@ var app = builder.Build();
|
|||||||
|
|
||||||
app.UseExceptionHandler();
|
app.UseExceptionHandler();
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
if (app.Environment.IsDevelopment())
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
if (context.Request.Path.StartsWithSegments("/swagger"))
|
||||||
app.UseSwaggerUI();
|
{
|
||||||
|
var isEnabled = await context.RequestServices
|
||||||
|
.GetRequiredService<AppDbContext>()
|
||||||
|
.SystemFeatureSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Key == SystemFeatureKeys.SwaggerDocumentation)
|
||||||
|
.Select(x => (bool?)x.IsEnabled)
|
||||||
|
.SingleOrDefaultAsync(context.RequestAborted) ?? false;
|
||||||
|
if (!isEnabled)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerEndpoint(
|
||||||
|
$"/swagger/{swaggerDocumentVersion}/swagger.json",
|
||||||
|
$"大学教务管理系统 API {swaggerDocumentVersion}");
|
||||||
|
options.DocExpansion(DocExpansion.None);
|
||||||
|
options.DefaultModelsExpandDepth(-1);
|
||||||
|
options.DefaultModelExpandDepth(1);
|
||||||
|
options.EnableFilter();
|
||||||
|
});
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<PackageReference Include="coverlet.collector" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageReference Include="xunit" Version="2.5.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -33,6 +33,39 @@ public sealed class OperationsControllerTests
|
|||||||
Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
|
Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Swagger_documentation_is_closed_by_default_and_can_be_enabled()
|
||||||
|
{
|
||||||
|
var root = CreateTemporaryRoot();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var fixture = await OperationsFixture.CreateAsync(root);
|
||||||
|
|
||||||
|
var initial = await fixture.Controller.GetSwaggerSettings(
|
||||||
|
CancellationToken.None);
|
||||||
|
var initialSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||||
|
Assert.IsType<OkObjectResult>(initial.Result).Value);
|
||||||
|
Assert.False(initialSettings.IsEnabled);
|
||||||
|
|
||||||
|
var updated = await fixture.Controller.UpdateSwaggerSettings(
|
||||||
|
new UpdateSwaggerDocumentationSettings(true),
|
||||||
|
CancellationToken.None);
|
||||||
|
var updatedSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||||
|
Assert.IsType<OkObjectResult>(updated.Result).Value);
|
||||||
|
Assert.True(updatedSettings.IsEnabled);
|
||||||
|
|
||||||
|
var persisted = await fixture.Controller.GetSwaggerSettings(
|
||||||
|
CancellationToken.None);
|
||||||
|
var persistedSettings = Assert.IsType<SwaggerDocumentationSettings>(
|
||||||
|
Assert.IsType<OkObjectResult>(persisted.Result).Value);
|
||||||
|
Assert.True(persistedSettings.IsEnabled);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteTemporaryRoot(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Audit_and_failed_job_queries_return_operational_records()
|
public async Task Audit_and_failed_job_queries_return_operational_records()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<JiaowuBackendVersion>2.3.2-beta.2</JiaowuBackendVersion>
|
||||||
|
<JiaowuFrontendVersion>2.3.2-beta.2</JiaowuFrontendVersion>
|
||||||
|
<JiaowuSwaggerVersion>2.3.2-beta.2</JiaowuSwaggerVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
Generated
+6
@@ -1,12 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
|
<<<<<<< Updated upstream
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
=======
|
||||||
|
>>>>>>> Stashed changes
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
|
<<<<<<< Updated upstream
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
=======
|
||||||
|
>>>>>>> Stashed changes
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.4.2",
|
"@capacitor/android": "^8.4.2",
|
||||||
"@capacitor/barcode-scanner": "^3.1.0",
|
"@capacitor/barcode-scanner": "^3.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.3.2-beta.1",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -98,12 +98,18 @@ interface PageResult<T> {
|
|||||||
pageSize: number
|
pageSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SwaggerDocumentationSettings {
|
||||||
|
isEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const backupBusy = ref(false)
|
const backupBusy = ref(false)
|
||||||
const summary = ref<Summary>()
|
const summary = ref<Summary>()
|
||||||
const backups = ref<BackupArtifact[]>([])
|
const backups = ref<BackupArtifact[]>([])
|
||||||
const activeLedger = ref<'audit' | 'jobs'>('audit')
|
const activeLedger = ref<'audit' | 'jobs'>('audit')
|
||||||
|
const swaggerEnabled = ref(false)
|
||||||
|
const swaggerBusy = ref(false)
|
||||||
|
|
||||||
const auditLoading = ref(false)
|
const auditLoading = ref(false)
|
||||||
const auditRows = ref<AuditLog[]>([])
|
const auditRows = ref<AuditLog[]>([])
|
||||||
@@ -204,12 +210,36 @@ function httpStatusType(status: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSummary() {
|
async function loadSummary() {
|
||||||
const [{ data: summaryData }, { data: backupData }] = await Promise.all([
|
const [{ data: summaryData }, { data: backupData }, { data: swaggerData }] = await Promise.all([
|
||||||
http.get<Summary>('/operations/summary'),
|
http.get<Summary>('/operations/summary'),
|
||||||
http.get<BackupArtifact[]>('/operations/backups'),
|
http.get<BackupArtifact[]>('/operations/backups'),
|
||||||
|
http.get<SwaggerDocumentationSettings>('/operations/swagger'),
|
||||||
])
|
])
|
||||||
summary.value = summaryData
|
summary.value = summaryData
|
||||||
backups.value = backupData
|
backups.value = backupData
|
||||||
|
swaggerEnabled.value = swaggerData.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSwaggerDocumentation(value: string | number | boolean) {
|
||||||
|
if (typeof value !== 'boolean') return
|
||||||
|
const isEnabled = value
|
||||||
|
swaggerBusy.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await http.put<SwaggerDocumentationSettings>(
|
||||||
|
'/operations/swagger',
|
||||||
|
{ isEnabled },
|
||||||
|
)
|
||||||
|
swaggerEnabled.value = data.isEnabled
|
||||||
|
ElMessage.success(data.isEnabled ? 'Swagger 文档已开放。' : 'Swagger 文档已关闭。')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
swaggerBusy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSwaggerDocumentation() {
|
||||||
|
window.open('/swagger', '_blank', 'noopener')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAudit() {
|
async function loadAudit() {
|
||||||
@@ -371,6 +401,35 @@ onMounted(refreshAll)
|
|||||||
</el-button>
|
</el-button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="swagger-panel">
|
||||||
|
<div>
|
||||||
|
<span>API DOCUMENTATION</span>
|
||||||
|
<h3>Swagger 文档</h3>
|
||||||
|
<p>
|
||||||
|
{{ swaggerEnabled
|
||||||
|
? '文档与 OpenAPI 定义当前已对外开放。'
|
||||||
|
: '文档当前关闭,访问 /swagger 将返回 404。' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="swagger-actions">
|
||||||
|
<el-switch
|
||||||
|
:model-value="swaggerEnabled"
|
||||||
|
:loading="swaggerBusy"
|
||||||
|
active-text="已开放"
|
||||||
|
inactive-text="已关闭"
|
||||||
|
@change="updateSwaggerDocumentation"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="swaggerEnabled"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openSwaggerDocumentation"
|
||||||
|
>
|
||||||
|
打开文档
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="signal-board" aria-label="系统健康链路">
|
<section class="signal-board" aria-label="系统健康链路">
|
||||||
<div class="signal-summary">
|
<div class="signal-summary">
|
||||||
<span>当前值守结论</span>
|
<span>当前值守结论</span>
|
||||||
@@ -670,6 +729,28 @@ onMounted(refreshAll)
|
|||||||
padding: 4px 2px 16px;
|
padding: 4px 2px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.swagger-panel {
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-left: 4px solid var(--signal);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-panel span {
|
||||||
|
color: var(--signal);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swagger-panel h3 { margin: 4px 0; }
|
||||||
|
.swagger-panel p { color: var(--muted); margin: 0; }
|
||||||
|
.swagger-actions { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
|
||||||
.console-kicker,
|
.console-kicker,
|
||||||
.panel-heading span,
|
.panel-heading span,
|
||||||
.signal-summary > span,
|
.signal-summary > span,
|
||||||
@@ -1011,6 +1092,7 @@ onMounted(refreshAll)
|
|||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.operations-console { gap: 12px; }
|
.operations-console { gap: 12px; }
|
||||||
.console-heading { align-items: flex-start; gap: 14px; }
|
.console-heading { align-items: flex-start; gap: 14px; }
|
||||||
|
.swagger-panel { align-items: flex-start; flex-direction: column; }
|
||||||
.console-heading p { font-size: 12px; line-height: 1.5; }
|
.console-heading p { font-size: 12px; line-height: 1.5; }
|
||||||
.signal-track { display: block; }
|
.signal-track { display: block; }
|
||||||
.signal-node { border-bottom: 1px solid #e4e9ea; padding: 13px 12px; }
|
.signal-node { border-bottom: 1px solid #e4e9ea; padding: 13px 12px; }
|
||||||
|
|||||||
+7
-4
@@ -5,9 +5,12 @@ import AutoImport from 'unplugin-auto-import/vite'
|
|||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
const packageJson = JSON.parse(
|
const versions = readFileSync(new URL('../versions.props', import.meta.url), 'utf8')
|
||||||
readFileSync(new URL('./package.json', import.meta.url), 'utf8'),
|
const frontendVersion = /<JiaowuFrontendVersion>([^<]+)<\/JiaowuFrontendVersion>/.exec(versions)?.[1]
|
||||||
) as { version: string }
|
|
||||||
|
if (!frontendVersion) {
|
||||||
|
throw new Error('versions.props 中缺少 JiaowuFrontendVersion。')
|
||||||
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
@@ -16,7 +19,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(packageJson.version),
|
__APP_VERSION__: JSON.stringify(frontendVersion),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
|
|||||||
Reference in New Issue
Block a user