From d4a8e71733bdf6a7e2a31f5748395dda8da0e1f9 Mon Sep 17 00:00:00 2001 From: GaoXiang Date: Mon, 30 Mar 2026 11:07:30 +0800 Subject: [PATCH] as is --- .gitignore | 2 +- Directory.Build.props | 7 + .../Extensions/AuthExtensions.cs | 56 +++++++ .../BearerOpenApiDocumentTransformer.cs | 37 +++++ .../Extensions/CommonServiceCollections.cs | 76 ++++++++++ .../EnumOpenApiSchemaTransformer.cs | 49 ++++++ .../Extensions/HttpExtensions.cs | 35 +++++ .../Extensions/MiddlewareExtensions.cs | 13 ++ .../GlobalExceptionHandlerMiddleware.cs | 106 +++++++++++++ .../Middlewares/ProblemDetailsExtensions.cs | 23 +++ StopShopping.AdminApi/Program.cs | 98 ++++++++++++ .../Properties/launchSettings.json | 23 +++ StopShopping.AdminApi/Routes/Admin.cs | 89 +++++++++++ StopShopping.AdminApi/Routes/Category.cs | 54 +++++++ StopShopping.AdminApi/Routes/Common.cs | 43 ++++++ StopShopping.AdminApi/Routes/District.cs | 34 +++++ StopShopping.AdminApi/Routes/Product.cs | 61 ++++++++ StopShopping.AdminApi/Routes/Root.cs | 35 +++++ .../StopShopping.AdminApi.csproj | 20 +++ .../Workers/DbSeederBackgroundService.cs | 2 +- .../appsettings.Template.json | 37 +++++ .../Extensions/CommonServiceCollections.cs | 11 +- StopShopping.Api/Extensions/HttpExtensions.cs | 9 +- .../Extensions/MiddlewareExtensions.cs | 12 -- .../DevelopmentCookieMiddleware.cs | 142 ------------------ .../GlobalExceptionHandlerMiddleware.cs | 38 +++-- StopShopping.Api/Program.cs | 26 +++- StopShopping.Api/Routes/Admin.cs | 44 ------ StopShopping.Api/Routes/Category.cs | 40 +---- StopShopping.Api/Routes/Common.cs | 50 +----- StopShopping.Api/Routes/Product.cs | 8 + StopShopping.Api/Routes/Request.cs | 12 +- StopShopping.Api/Routes/Root.cs | 19 +-- StopShopping.Api/Routes/User.cs | 56 ++++++- StopShopping.Api/StopShopping.Api.csproj | 6 - StopShopping.Api/appsettings.Template.json | 3 +- .../EnumOpenApiSchemaTransformer.cs | 49 ++++++ .../Extensions/MiddlewareExtensions.cs | 13 ++ .../Extensions/RouteGroupBuilderExtensions.cs | 13 ++ .../InternalAccessOnlyMiddleware.cs | 58 +++++++ StopShopping.FileApi/Program.cs | 69 +++++++++ .../Properties/launchSettings.json | 23 +++ StopShopping.FileApi/Routes.cs | 31 ++++ StopShopping.FileApi/Services/ApiResponse.cs | 64 ++++++++ StopShopping.FileApi/Services/AppOptions.cs | 6 + StopShopping.FileApi/Services/Extensions.cs | 34 +++++ .../Services/FileUploadResp.cs | 18 +++ StopShopping.FileApi/Services/IFileService.cs | 14 ++ .../Services/Implementions/FileService.cs | 59 ++++++++ .../Services/NameUrlParams.cs | 22 +++ StopShopping.FileApi/Services/NameUrlResp.cs | 14 ++ StopShopping.FileApi/Services/UploadParams.cs | 23 +++ .../Services/UploadScences.cs | 20 +++ .../Validator/ImageFileValidationAttribute.cs | 2 - .../StopShopping.FileApi.csproj | 25 +++ .../appsettings.Template.json | 26 ++++ StopShopping.Services/Consts.cs | 5 + .../Extensions/AppOptions.cs | 15 +- .../Extensions/ServicesExtensions.cs | 13 +- StopShopping.Services/IClaimsService.cs | 2 +- StopShopping.Services/IFileService.cs | 2 +- .../Implementions/CategoryService.cs | 5 +- .../Implementions/ClaimsService.cs | 4 +- .../Implementions/FileService.cs | 87 +++++++---- .../Implementions/ProductService.cs | 16 +- .../Implementions/ReplyService.cs | 2 +- .../Implementions/RequestService.cs | 6 +- .../Implementions/UserService.cs | 12 +- .../Models/Req/NameUrlParams.cs | 22 +++ .../Models/Req/UploadParams.cs | 3 +- .../Resp/{FileUpload.cs => FileUploadResp.cs} | 2 +- .../Models/Resp/NameUrlResp.cs | 14 ++ .../StopShopping.Services.csproj | 1 - StopShopping.slnx | 2 + 74 files changed, 1751 insertions(+), 421 deletions(-) create mode 100644 StopShopping.AdminApi/Extensions/AuthExtensions.cs create mode 100644 StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs create mode 100644 StopShopping.AdminApi/Extensions/CommonServiceCollections.cs create mode 100644 StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs create mode 100644 StopShopping.AdminApi/Extensions/HttpExtensions.cs create mode 100644 StopShopping.AdminApi/Extensions/MiddlewareExtensions.cs create mode 100644 StopShopping.AdminApi/Middlewares/GlobalExceptionHandlerMiddleware.cs create mode 100644 StopShopping.AdminApi/Middlewares/ProblemDetailsExtensions.cs create mode 100644 StopShopping.AdminApi/Program.cs create mode 100644 StopShopping.AdminApi/Properties/launchSettings.json create mode 100644 StopShopping.AdminApi/Routes/Admin.cs create mode 100644 StopShopping.AdminApi/Routes/Category.cs create mode 100644 StopShopping.AdminApi/Routes/Common.cs create mode 100644 StopShopping.AdminApi/Routes/District.cs create mode 100644 StopShopping.AdminApi/Routes/Product.cs create mode 100644 StopShopping.AdminApi/Routes/Root.cs create mode 100644 StopShopping.AdminApi/StopShopping.AdminApi.csproj rename {StopShopping.Api => StopShopping.AdminApi}/Workers/DbSeederBackgroundService.cs (94%) create mode 100644 StopShopping.AdminApi/appsettings.Template.json delete mode 100644 StopShopping.Api/Middlewares/DevelopmentCookieMiddleware.cs delete mode 100644 StopShopping.Api/Routes/Admin.cs create mode 100644 StopShopping.FileApi/Extensions/EnumOpenApiSchemaTransformer.cs create mode 100644 StopShopping.FileApi/Extensions/MiddlewareExtensions.cs create mode 100644 StopShopping.FileApi/Extensions/RouteGroupBuilderExtensions.cs create mode 100644 StopShopping.FileApi/Middlewares/InternalAccessOnlyMiddleware.cs create mode 100644 StopShopping.FileApi/Program.cs create mode 100644 StopShopping.FileApi/Properties/launchSettings.json create mode 100644 StopShopping.FileApi/Routes.cs create mode 100644 StopShopping.FileApi/Services/ApiResponse.cs create mode 100644 StopShopping.FileApi/Services/AppOptions.cs create mode 100644 StopShopping.FileApi/Services/Extensions.cs create mode 100644 StopShopping.FileApi/Services/FileUploadResp.cs create mode 100644 StopShopping.FileApi/Services/IFileService.cs create mode 100644 StopShopping.FileApi/Services/Implementions/FileService.cs create mode 100644 StopShopping.FileApi/Services/NameUrlParams.cs create mode 100644 StopShopping.FileApi/Services/NameUrlResp.cs create mode 100644 StopShopping.FileApi/Services/UploadParams.cs create mode 100644 StopShopping.FileApi/Services/UploadScences.cs rename {StopShopping.Services/Models => StopShopping.FileApi/Services}/Validator/ImageFileValidationAttribute.cs (94%) create mode 100644 StopShopping.FileApi/StopShopping.FileApi.csproj create mode 100644 StopShopping.FileApi/appsettings.Template.json create mode 100644 StopShopping.Services/Models/Req/NameUrlParams.cs rename StopShopping.Services/Models/Resp/{FileUpload.cs => FileUploadResp.cs} (93%) create mode 100644 StopShopping.Services/Models/Resp/NameUrlResp.cs diff --git a/.gitignore b/.gitignore index 5c35998..11ba6ae 100644 --- a/.gitignore +++ b/.gitignore @@ -482,6 +482,6 @@ $RECYCLE.BIN/ *.swp doc/password.txt -StopShopping.Api/wwwroot/images/ +wwwroot/ appsettings.json appsettings.Development.json \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 5bee49f..5048c78 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,4 +6,11 @@ latest Recommended + + + + Never + + + \ No newline at end of file diff --git a/StopShopping.AdminApi/Extensions/AuthExtensions.cs b/StopShopping.AdminApi/Extensions/AuthExtensions.cs new file mode 100644 index 0000000..4d6ba8a --- /dev/null +++ b/StopShopping.AdminApi/Extensions/AuthExtensions.cs @@ -0,0 +1,56 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; +using StopShopping.Services; + +namespace StopShopping.AdminApi.Extensions; + +public static class JwtExtensions +{ + public static IServiceCollection AddAuthServices(this IServiceCollection services, IConfiguration jwtOptions) + { + services.Configure(jwtOptions); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(jwtBearerOptions => + { + var jwtConfiguration = jwtOptions.Get()!; + + var signingKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtConfiguration.SigningKey!) + ); + + jwtBearerOptions.MapInboundClaims = false; + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters + { + ValidAudience = jwtConfiguration.ValidAudience, + ValidIssuer = jwtConfiguration.ValidIssuer, + IssuerSigningKey = signingKey, + ClockSkew = TimeSpan.FromSeconds(30) //宽容时间,30秒后才失效 + }; + jwtBearerOptions.Events = new JwtBearerEvents + { + OnMessageReceived = async (context) => + { + var accessTokenService = context.HttpContext.RequestServices.GetRequiredService(); + var authorizationHeader = context.Request.Headers[HeaderNames.Authorization]; + if (authorizationHeader.Count == 0) + { + context.Fail($"未找到{HeaderNames.Authorization}请求头"); + } + else + { + var token = authorizationHeader.First()!.Split(" ").Last(); + if (string.IsNullOrWhiteSpace(token)) + context.Fail("未找到token"); + if (await accessTokenService.IsAccessTokenBlacklistAsync(token)) + context.Fail("token已失效"); + } + } + }; + }); + services.AddAuthorization(); + + return services; + } +} \ No newline at end of file diff --git a/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs b/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs new file mode 100644 index 0000000..5db4caa --- /dev/null +++ b/StopShopping.AdminApi/Extensions/BearerOpenApiDocumentTransformer.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace StopShopping.AdminApi.Extensions; + +// 来自网络。。。 +public class BearerOpenApiDocumentTransformer : IOpenApiDocumentTransformer +{ + public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) + { + var bearerOpenApiSecurityScheme = new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Scheme = JwtBearerDefaults.AuthenticationScheme, + Type = SecuritySchemeType.Http, + Description = "jwt" + }; + + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes ??= new Dictionary(); + document.Components.SecuritySchemes[JwtBearerDefaults.AuthenticationScheme] = bearerOpenApiSecurityScheme; + + var securityRequirement = new OpenApiSecurityRequirement + { + { + new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme), + new List() + } + }; + + document.Security ??= []; + document.Security.Add(securityRequirement); + + return Task.CompletedTask; + } +} diff --git a/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs b/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs new file mode 100644 index 0000000..a64d385 --- /dev/null +++ b/StopShopping.AdminApi/Extensions/CommonServiceCollections.cs @@ -0,0 +1,76 @@ +using System.Text.Json.Serialization; +using StopShopping.AdminApi.Middlewares; +using StopShopping.Services; +using StopShopping.Services.Extensions; + +namespace StopShopping.AdminApi.Extensions; + +public static class CommonServiceCollections +{ + public static IServiceCollection AddCommonServices( + this IServiceCollection services, + string corsPolicy, + IConfigurationSection jwtConfiguration, + IConfigurationSection appConfiguration, + bool isDevelopment) + { + var appOptions = appConfiguration.Get(); + services.AddCors(options => + { + options.AddPolicy(corsPolicy, policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.WithOrigins(appOptions!.CorsAllowedOrigins); + policy.AllowCredentials(); + }); + }); + services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add( + new JsonStringEnumConverter(namingPolicy: null, allowIntegerValues: true)); + }); + services.AddHttpContextAccessor(); + services.AddOpenApi(options => + { + options.AddDocumentTransformer(); + options.AddSchemaTransformer(); + }); + services.AddProblemDetails(options => + { + options.CustomizeProblemDetails = (context) => + { + if (context.ProblemDetails is HttpValidationProblemDetails problemDetails) + { + problemDetails.AddErrorCode(ProblemDetailsCodes.ParametersValidationFailed); + var errors = problemDetails.Errors.Select(e => string.Join(',', e.Value)); + if (null != errors) + problemDetails.Detail = string.Join(',', errors); + } + }; + }); + services.AddValidation(); + services.AddDistributedMemoryCache(); + + services.AddAuthServices(jwtConfiguration); + + services.AddAntiforgery(options => + { + var jwtOptions = jwtConfiguration.Get(); + + options.HeaderName = appOptions!.CSRFHeaderName; + options.Cookie.MaxAge = TimeSpan.FromSeconds(jwtOptions!.RefreshTokenExpiresIn); + options.Cookie.HttpOnly = true; + options.Cookie.Name = appOptions.CSRFCookieName; + + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.Domain = appOptions.CookieDomain; + if (!isDevelopment) + { + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + } + }); + + return services; + } +} diff --git a/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs b/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs new file mode 100644 index 0000000..725ffb1 --- /dev/null +++ b/StopShopping.AdminApi/Extensions/EnumOpenApiSchemaTransformer.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace StopShopping.AdminApi.Extensions; + +/// +/// 处理enum类型openapi显示 +/// +public class EnumOpenApiSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + if (context.JsonTypeInfo.Type.IsEnum) + { + schema.Type = JsonSchemaType.Integer; + + var enumValues = Enum.GetValues(context.JsonTypeInfo.Type) + .Cast() + .Select(v => JsonNode.Parse(Convert.ToInt32(v).ToString())!) + .ToList(); + + schema.Enum = enumValues; + + var enumNames = Enum.GetNames(context.JsonTypeInfo.Type); + schema.Extensions ??= new Dictionary(); + var namesExtension = new JsonNodeExtension(new JsonArray( + enumNames + .Select(n => (JsonNode)n) + .ToArray())); + schema.Extensions.Add("x-enumNames", namesExtension); + + var descMap = new JsonObject(); + foreach (var name in enumNames) + { + if (context.JsonTypeInfo.Type.GetField(name) + ?.GetCustomAttributes(typeof(DescriptionAttribute), false) + .FirstOrDefault() is DescriptionAttribute attr) + { + descMap[name] = attr.Description; + } + } + schema.Extensions.Add("x-enumDescriptions", new JsonNodeExtension(descMap)); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/StopShopping.AdminApi/Extensions/HttpExtensions.cs b/StopShopping.AdminApi/Extensions/HttpExtensions.cs new file mode 100644 index 0000000..30102ac --- /dev/null +++ b/StopShopping.AdminApi/Extensions/HttpExtensions.cs @@ -0,0 +1,35 @@ + +using StopShopping.Services.Extensions; + +namespace Microsoft.AspNetCore.Http; + +public static class HttpExtensions +{ + public const string REFRESH_TOKEN_COOKIE_KEY = "admin_refresh_token"; + + public static IResponseCookies AppendRefreshToken( + this IResponseCookies cookies, + IWebHostEnvironment env, + AppOptions appOptions, + TimeSpan maxAge, + string token) + { + CookieOptions options = new() + { + MaxAge = maxAge, + HttpOnly = true, + SameSite = SameSiteMode.Lax, + Domain = appOptions.CookieDomain, + }; + + if (!env.IsDevelopment()) + options.Secure = true; + + cookies.Append( + REFRESH_TOKEN_COOKIE_KEY, + token, + options); + + return cookies; + } +} diff --git a/StopShopping.AdminApi/Extensions/MiddlewareExtensions.cs b/StopShopping.AdminApi/Extensions/MiddlewareExtensions.cs new file mode 100644 index 0000000..e06ac5f --- /dev/null +++ b/StopShopping.AdminApi/Extensions/MiddlewareExtensions.cs @@ -0,0 +1,13 @@ +using StopShopping.AdminApi.Middlewares; + +namespace Microsoft.AspNetCore.Builder; + +public static class MiddlewareExtensions +{ + public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseMiddleware(); + + return applicationBuilder; + } +} diff --git a/StopShopping.AdminApi/Middlewares/GlobalExceptionHandlerMiddleware.cs b/StopShopping.AdminApi/Middlewares/GlobalExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..4fce60f --- /dev/null +++ b/StopShopping.AdminApi/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -0,0 +1,106 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Mvc; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Middlewares; + +public class GlobalExceptionHandlerMiddleware +{ + public GlobalExceptionHandlerMiddleware(RequestDelegate requestDelegate, + ILogger logger, + IProblemDetailsService problemDetailsService) + { + _next = requestDelegate; + _logger = logger; + _problemDetailsService = problemDetailsService; + } + + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IProblemDetailsService _problemDetailsService; + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + await _next(httpContext); + httpContext.Response.OnStarting(async () => + { + var antiforgeryFeature = httpContext.Features.Get(); + if (null != antiforgeryFeature && !antiforgeryFeature.IsValid) + { + var problemDetails = new ProblemDetails + { + Detail = antiforgeryFeature.Error?.Message, + Instance = httpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Title = "CSRF 错误", + }; + + problemDetails.AddErrorCode(ProblemDetailsCodes.CsrfValidationFailed); + + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + httpContext.Response.ContentType = "application/problem+json"; + + await _problemDetailsService.WriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, + ProblemDetails = problemDetails + }); + } + + await Task.CompletedTask; + }); + } + catch (BadHttpRequestException ex) + { + _logger.LogError(ex, "参数错误"); + + var problemDetails = new ProblemDetails + { + Detail = ex.Message, + Instance = httpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Title = "参数错误", + }; + + problemDetails.AddErrorCode(ProblemDetailsCodes.BadParameters); + + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + httpContext.Response.ContentType = "application/problem+json"; + + await _problemDetailsService.WriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, + ProblemDetails = problemDetails + }); + } + catch (ServiceException ex) + { + await httpContext.Response.WriteAsJsonAsync(ApiResponse.Failed(ex.Message)); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "意外的异常"); + + var problemDetails = new ProblemDetails + { + Detail = ex.Message, + Instance = httpContext.Request.Path, + Status = StatusCodes.Status500InternalServerError, + Title = "服务器错误", + }; + + problemDetails.AddErrorCode(ProblemDetailsCodes.ServerError); + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + httpContext.Response.ContentType = "application/problem+json"; + + await _problemDetailsService.WriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, + ProblemDetails = problemDetails, + }); + } + } +} diff --git a/StopShopping.AdminApi/Middlewares/ProblemDetailsExtensions.cs b/StopShopping.AdminApi/Middlewares/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..6ebfc06 --- /dev/null +++ b/StopShopping.AdminApi/Middlewares/ProblemDetailsExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; + +namespace StopShopping.AdminApi.Middlewares; + +public static class ProblemDetailsExtensions +{ + private const string CODE_FIELD = "code"; + public static ProblemDetails AddErrorCode(this ProblemDetails problemDetails, ProblemDetailsCodes code) + { + problemDetails.Extensions ??= new Dictionary(); + + problemDetails.Extensions.Add(CODE_FIELD, (int)code); + return problemDetails; + } +} + +public enum ProblemDetailsCodes +{ + CsrfValidationFailed = 1000, + ParametersValidationFailed = 1001, + BadParameters = 1002, + ServerError = 1003, +} diff --git a/StopShopping.AdminApi/Program.cs b/StopShopping.AdminApi/Program.cs new file mode 100644 index 0000000..bc77039 --- /dev/null +++ b/StopShopping.AdminApi/Program.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.HostFiltering; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Scalar.AspNetCore; +using Serilog; +using StopShopping.AdminApi.Extensions; +using StopShopping.AdminApi.Routes; +using StopShopping.AdminApi.Workers; + +const string CORS_POLICY = "default"; +// 将启动日志写入控制台,用于捕获启动时异常,启动后WriteTo被后续配置替代 +Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateBootstrapLogger(); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddSerilog((services, seriLogger) => + { + seriLogger.ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services); + }); + + var jwtConfiguration = builder.Configuration.GetSection("JwtOptions"); + var appConfiguration = builder.Configuration.GetSection("AppOptions"); + + builder.Services.AddCommonServices( + CORS_POLICY, + jwtConfiguration, + appConfiguration, + builder.Environment.IsDevelopment()); + + builder.Services.AddServices(dbContextOptions => + { + dbContextOptions.UseNpgsql( + builder.Configuration.GetConnectionString("StopShopping")); + + if (builder.Environment.IsDevelopment()) + dbContextOptions.EnableSensitiveDataLogging(); + }, + appConfiguration, + builder.Configuration.GetSection("OpenPlatformOptions")); + builder.Services.AddHostedService(); + /**********************************************************************/ + var app = builder.Build(); + + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.MapScalarApiReference(options => + { + options.AddPreferredSecuritySchemes(JwtBearerDefaults.AuthenticationScheme); + }); + } + + if (!app.Environment.IsDevelopment()) + { + app.UseHostFiltering(); + + var forwardedHeadersOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.All, + }; + var hostFilteringOptions = app.Services.GetRequiredService>(); + if (null != hostFilteringOptions) + forwardedHeadersOptions.AllowedHosts = hostFilteringOptions.Value.AllowedHosts; + app.UseForwardedHeaders(forwardedHeadersOptions); + } + + app.UseGlobalExceptionHandler(); + + app.UseCors(CORS_POLICY); + + app.UseRouting(); + + app.UseAuthentication(); + + app.UseAuthorization(); + + Root.MapRoutes(app); + + app.UseAntiforgery(); + + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "启动异常!"); +} +finally +{ + Log.CloseAndFlush(); +} + diff --git a/StopShopping.AdminApi/Properties/launchSettings.json b/StopShopping.AdminApi/Properties/launchSettings.json new file mode 100644 index 0000000..6e4d4ea --- /dev/null +++ b/StopShopping.AdminApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7054;http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/StopShopping.AdminApi/Routes/Admin.cs b/StopShopping.AdminApi/Routes/Admin.cs new file mode 100644 index 0000000..407c8e7 --- /dev/null +++ b/StopShopping.AdminApi/Routes/Admin.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using StopShopping.Services; +using StopShopping.Services.Extensions; +using StopShopping.Services.Models.Req; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Routes; + +public static class Admin +{ + public static RouteGroupBuilder MapAdmin(this RouteGroupBuilder routes) + { + routes.MapPost("/admin/signin", SignInAsync) + .WithTags(OpenApiTags.管理员.ToString()); + + routes.MapPost("/admin/refreshtoken", RefreshTokenAsync) + .Produces>() + .WithTags(OpenApiTags.管理员.ToString()); + + routes.MapPost("/admin/signout", SignOutAsync) + .WithTags(OpenApiTags.管理员.ToString()); + + return routes; + } + + private static async Task> SignInAsync( + SignInParams model, + IUserService userService, + HttpContext httpContext, + IWebHostEnvironment env, + IOptions options) + { + var result = await userService.SignInAdminAsync(model); + var resp = new ApiResponse + { + IsSucced = result.IsSucced, + Data = result.User, + Message = result.Message + }; + if (result.IsSucced) + { + httpContext.Response.Cookies.AppendRefreshToken( + env, + options.Value, + TimeSpan.FromSeconds(result.RefreshToken!.ExpiresIn), + result.RefreshToken.Token! + ); + } + return resp; + } + + private static async Task RefreshTokenAsync( + HttpContext httpContext, + IAccessTokenService accessTokenService) + { + var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; + if (string.IsNullOrWhiteSpace(refreshToken)) + return Results.Unauthorized(); + + var accessToken = await accessTokenService.GenerateAccessTokenAsync(refreshToken); + if (null == accessToken) + return Results.Unauthorized(); + + return Results.Ok(new ApiResponse(accessToken)); + } + + public static async Task SignOutAsync( + HttpContext httpContext, + IAccessTokenService accessTokenService) + { + var accessTokenHeader = httpContext.Request.Headers[HeaderNames.Authorization]; + if (accessTokenHeader.Count != 0) + { + var accessToken = accessTokenHeader.First()!.Split(" ").Last(); + if (!string.IsNullOrWhiteSpace(accessToken)) + await accessTokenService.AddAccessTokenBlacklistAsync(accessToken); + } + + var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; + if (!string.IsNullOrWhiteSpace(refreshToken)) + { + await accessTokenService.RevokeRefreshTokenAsync(refreshToken); + httpContext.Response.Cookies.Delete(HttpExtensions.REFRESH_TOKEN_COOKIE_KEY); + } + + return ApiResponse.Succed(); + } +} diff --git a/StopShopping.AdminApi/Routes/Category.cs b/StopShopping.AdminApi/Routes/Category.cs new file mode 100644 index 0000000..71199b0 --- /dev/null +++ b/StopShopping.AdminApi/Routes/Category.cs @@ -0,0 +1,54 @@ +using StopShopping.Services; +using StopShopping.Services.Models.Req; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Routes; + +public static class Category +{ + public static RouteGroupBuilder MapCategory(this RouteGroupBuilder routes) + { + routes.MapGet("/category/list", GetTree) + .WithTags(OpenApiTags.分类.ToString()); + + routes.MapPost("/category/edit", EditCategoryAsync) + .WithTags(OpenApiTags.分类.ToString()); + + routes.MapPost("/category/resort", ResortCategoryAsync) + .WithTags(OpenApiTags.分类.ToString()); + + routes.MapPost("/category/delete", DeleteCategoryAsync) + .WithTags(OpenApiTags.分类.ToString()); + + return routes; + } + + private static ApiResponse> GetTree( + ICategoryService categoryService + ) + { + return categoryService.GetCategoriesTree(); + } + + private static async Task> EditCategoryAsync( + EditCategoryParams model, + ICategoryService categoryService) + { + return await categoryService.EditCategoryAsync(model); + } + + private static async Task ResortCategoryAsync( + ResortCategoryParams model, + ICategoryService categoryService) + { + return await categoryService.ResortCategoryAsync(model); + } + + private static async Task DeleteCategoryAsync( + CategoryIdParams model, + ICategoryService categoryService + ) + { + return await categoryService.DeleteCategoryAsync(model); + } +} diff --git a/StopShopping.AdminApi/Routes/Common.cs b/StopShopping.AdminApi/Routes/Common.cs new file mode 100644 index 0000000..9d96c12 --- /dev/null +++ b/StopShopping.AdminApi/Routes/Common.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Antiforgery; +using Microsoft.AspNetCore.Mvc; +using StopShopping.Services; +using StopShopping.Services.Models.Req; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Routes; + +public static class Common +{ + public static RouteGroupBuilder MapCommon(this RouteGroupBuilder routes) + { + routes.MapPost("/common/upload", UploadAsync) + .WithTags(OpenApiTags.通用.ToString()); + + + routes.MapPost("/common/antiforgery-token", AntiForgeryToken) + .WithTags(OpenApiTags.通用.ToString()); + + return routes; + } + + private static async Task> UploadAsync( + [FromForm] UploadParams payload, + IFileService fileService, + HttpContext httpContext) + { + return await fileService.UploadFileAsync(payload); + } + + private static ApiResponse AntiForgeryToken( + HttpContext httpContext, + IAntiforgery antiforgery) + { + var antiforgeryToken = antiforgery.GetAndStoreTokens(httpContext); + + return new ApiResponse(new AntiForgeryToken + { + Token = antiforgeryToken.RequestToken, + HeaderName = antiforgeryToken.HeaderName + }); + } +} diff --git a/StopShopping.AdminApi/Routes/District.cs b/StopShopping.AdminApi/Routes/District.cs new file mode 100644 index 0000000..cbd2ea3 --- /dev/null +++ b/StopShopping.AdminApi/Routes/District.cs @@ -0,0 +1,34 @@ +using StopShopping.Services; +using StopShopping.Services.Models.Req; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Routes; + +public static class District +{ + public static RouteGroupBuilder MapDistrict(this RouteGroupBuilder routes) + { + routes.MapGet("/district/top3level", GetTop3LevelDistrictsAsync) + .WithTags(OpenApiTags.地址.ToString()); + + routes.MapGet("/district/children", GetChildrenDistricts) + .WithTags(OpenApiTags.地址.ToString()); + + return routes; + } + + private static ApiResponse> GetChildrenDistricts( + [AsParameters] DistrictParentIdParams model, + IDistrictService districtService + ) + { + return districtService.GetChildren(model); + } + + private static async Task>> GetTop3LevelDistrictsAsync( + IDistrictService districtService + ) + { + return await districtService.GetTop3LevelDistrictsAsync(); + } +} diff --git a/StopShopping.AdminApi/Routes/Product.cs b/StopShopping.AdminApi/Routes/Product.cs new file mode 100644 index 0000000..8ef344e --- /dev/null +++ b/StopShopping.AdminApi/Routes/Product.cs @@ -0,0 +1,61 @@ +using StopShopping.Services; +using StopShopping.Services.Models.Req; +using StopShopping.Services.Models.Resp; + +namespace StopShopping.AdminApi.Routes; + +/// +/// 商品相关路由 +/// +public static class Product +{ + public static RouteGroupBuilder MapProduct(this RouteGroupBuilder routes) + { + routes.MapGet("/product/list", SearchProductsAsync) + .WithTags(OpenApiTags.商品.ToString()); + + routes.MapGet("/product/detail", Detail) + .WithTags(OpenApiTags.商品.ToString()); + + routes.MapPost("/product/edit", EditAsync) + .WithTags(OpenApiTags.商品.ToString()); + + routes.MapPost("/product/delete", DeleteAsync) + .WithTags(OpenApiTags.商品.ToString()); + + return routes; + } + + private static async + Task>> + SearchProductsAsync( + [AsParameters] ProductSearchParms model, + IProductService productService + ) + { + return await productService.SearchAsync(model); + } + + private static ApiResponse Detail( + [AsParameters] ProductIdParams model, + IProductService productService) + { + return productService.Detail(model); + } + + private static async Task EditAsync( + EditProductParams model, + IProductService productService + ) + { + return await productService.EditAsync(model); + } + + private static async Task DeleteAsync( + ProductIdParams model, + IProductService productService + ) + { + return await productService.DeleteAsync(model); + } +} diff --git a/StopShopping.AdminApi/Routes/Root.cs b/StopShopping.AdminApi/Routes/Root.cs new file mode 100644 index 0000000..f2fd294 --- /dev/null +++ b/StopShopping.AdminApi/Routes/Root.cs @@ -0,0 +1,35 @@ + +using Scalar.AspNetCore; +using StopShopping.Services.Models; + +namespace StopShopping.AdminApi.Routes; + +/// +/// 其他路由从RouteGroupBuilder扩展并添加到MapGroup之后 +/// +public static class Root +{ + public static void MapRoutes(WebApplication app) + { + app.MapGroup("") + .MapCommon() + .MapCategory() + .MapProduct() + .MapDistrict() + .WithDescription("登录用户调用") + .RequireAuthorization(policy => policy.RequireRole(SystemRoles.Admin.ToString())); + app.MapGroup("") + .MapAdmin() + .WithDescription("公共调用") + .AllowAnonymous(); + } +} + +public enum OpenApiTags +{ + 分类, + 商品, + 管理员, + 通用, + 地址, +} diff --git a/StopShopping.AdminApi/StopShopping.AdminApi.csproj b/StopShopping.AdminApi/StopShopping.AdminApi.csproj new file mode 100644 index 0000000..b593c72 --- /dev/null +++ b/StopShopping.AdminApi/StopShopping.AdminApi.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + diff --git a/StopShopping.Api/Workers/DbSeederBackgroundService.cs b/StopShopping.AdminApi/Workers/DbSeederBackgroundService.cs similarity index 94% rename from StopShopping.Api/Workers/DbSeederBackgroundService.cs rename to StopShopping.AdminApi/Workers/DbSeederBackgroundService.cs index 7c1dc7c..3b4396f 100644 --- a/StopShopping.Api/Workers/DbSeederBackgroundService.cs +++ b/StopShopping.AdminApi/Workers/DbSeederBackgroundService.cs @@ -1,6 +1,6 @@ using StopShopping.Services; -namespace StopShopping.Api.Workers; +namespace StopShopping.AdminApi.Workers; public class DbSeederBackgroundService : BackgroundService { diff --git a/StopShopping.AdminApi/appsettings.Template.json b/StopShopping.AdminApi/appsettings.Template.json new file mode 100644 index 0000000..1fe8d61 --- /dev/null +++ b/StopShopping.AdminApi/appsettings.Template.json @@ -0,0 +1,37 @@ +{ + "ConnectionStrings": { + "StopShopping": "PG_CONNECTION_STRING" + }, + "JwtOptions": { + "ValidAudience": "StopShopping.Client", + "ValidIssuer": "StopShopping.Api", + "SigningKey": "128_BIT_SIGNING_KEY", + "AccessTokenExpiresIn": "3600", + "RefreshTokenExpiresIn": "604800" + }, + "AppOptions": { + "FileApiDomain": "FILE_API_DOMAIN", + "FileApiLocalDomain": "FILE_API_LOCAL_DOMAIN", + "CookieDomain": ".example.com或者localhost(开发环境)", + "CSRFHeaderName": "X-CSRF-TOKEN", + "CSRFCookieName": "csrf_token", + "CorsAllowedOrigins": ["https://web.example.com(跨域)"] + }, + "OpenPlatformOptions": { + "TencentDistrict": { + "SecretKey": "TENCENT_API_KEY", + "Top3LevelUrl": "https://apis.map.qq.com/ws/district/v1/list", + "GetChildrenUrl": "https://apis.map.qq.com/ws/district/v1/getchildren" + } + }, + "Serilog": { + "Using": ["Serilog.Sinks.Console"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning" + } + }, + "WriteTo": [{ "Name": "Console" }] + } +} diff --git a/StopShopping.Api/Extensions/CommonServiceCollections.cs b/StopShopping.Api/Extensions/CommonServiceCollections.cs index cdecf48..de14784 100644 --- a/StopShopping.Api/Extensions/CommonServiceCollections.cs +++ b/StopShopping.Api/Extensions/CommonServiceCollections.cs @@ -62,15 +62,12 @@ public static class CommonServiceCollections options.Cookie.MaxAge = TimeSpan.FromSeconds(jwtOptions!.RefreshTokenExpiresIn); options.Cookie.HttpOnly = true; options.Cookie.Name = appOptions.CSRFCookieName; - if (isDevelopment) + + options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.Domain = appOptions.CookieDomain; + if (!isDevelopment) { - options.Cookie.SameSite = SameSiteMode.Lax; - } - else - { - options.Cookie.SameSite = SameSiteMode.None; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; - options.Cookie.Domain = appOptions.CookieDomain; } }); diff --git a/StopShopping.Api/Extensions/HttpExtensions.cs b/StopShopping.Api/Extensions/HttpExtensions.cs index b1e4bb8..708acfe 100644 --- a/StopShopping.Api/Extensions/HttpExtensions.cs +++ b/StopShopping.Api/Extensions/HttpExtensions.cs @@ -1,10 +1,11 @@ + using StopShopping.Services.Extensions; namespace Microsoft.AspNetCore.Http; public static class HttpExtensions { - public const string REFRESH_TOKEN_COOKIE_KEY = "refresh_token"; + public const string REFRESH_TOKEN_COOKIE_KEY = "user_refresh_token"; public static IResponseCookies AppendRefreshToken( this IResponseCookies cookies, @@ -18,13 +19,11 @@ public static class HttpExtensions MaxAge = maxAge, HttpOnly = true, SameSite = SameSiteMode.Lax, + Domain = appOptions.CookieDomain, }; + if (!env.IsDevelopment()) - { - options.SameSite = SameSiteMode.None; options.Secure = true; - options.Domain = appOptions.CookieDomain; - } cookies.Append( REFRESH_TOKEN_COOKIE_KEY, diff --git a/StopShopping.Api/Extensions/MiddlewareExtensions.cs b/StopShopping.Api/Extensions/MiddlewareExtensions.cs index 858b816..7f55cd0 100644 --- a/StopShopping.Api/Extensions/MiddlewareExtensions.cs +++ b/StopShopping.Api/Extensions/MiddlewareExtensions.cs @@ -10,16 +10,4 @@ public static class MiddlewareExtensions return applicationBuilder; } - - /// - /// 解决开发时多客户端localhost端口串cookie的问题 - /// - /// - /// - public static IApplicationBuilder UseDevelopmentCookie(this IApplicationBuilder applicationBuilder) - { - applicationBuilder.UseMiddleware(); - - return applicationBuilder; - } } diff --git a/StopShopping.Api/Middlewares/DevelopmentCookieMiddleware.cs b/StopShopping.Api/Middlewares/DevelopmentCookieMiddleware.cs deleted file mode 100644 index 41a038d..0000000 --- a/StopShopping.Api/Middlewares/DevelopmentCookieMiddleware.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Text; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using StopShopping.Services.Extensions; - -namespace StopShopping.Api.Middlewares; - -public class DevelopmentCookieMiddleware -{ - public DevelopmentCookieMiddleware( - RequestDelegate requestDelegate, - IOptions options) - { - _next = requestDelegate; - _appOptions = options.Value; - } - - private readonly RequestDelegate _next; - private readonly AppOptions _appOptions; - - public async Task InvokeAsync(HttpContext httpContext) - { - int? port = null; - var origin = httpContext.Request.Headers[HeaderNames.Origin]; - if (origin.Count > 0 && null != origin[0]) - { - Uri uri = new(origin[0]!); - port = uri.Port; - } - else - { - var referer = httpContext.Request.Headers[HeaderNames.Referer]; - if (referer.Count > 0 && null != referer[0]) - { - Uri uri = new(referer[0]!); - port = uri.Port; - } - } - - if (port.HasValue) - { - var modified = ModifyCookie(httpContext.Request.Headers[HeaderNames.Cookie], port.Value); - httpContext.Request.Headers[HeaderNames.Cookie] = modified; - } - - httpContext.Response.OnStarting(() => - { - if (port.HasValue) - { - var cookieHeader = httpContext.Response.Headers[HeaderNames.SetCookie]; - ModifyResponseCookie(cookieHeader, httpContext, port.Value); - } - return Task.CompletedTask; - }); - - await _next(httpContext); - } - - private void ModifyResponseCookie(StringValues cookieHeader, HttpContext httpContext, int port) - { - foreach (var cookieItem in cookieHeader) - { - if (cookieItem != null) - { - var cookies = cookieItem.Split(';'); - foreach (var item in cookies) - { - if (null != item) - { - var pairs = item.Split('='); - if (2 == pairs.Length) - { - if (pairs[0].Trim() == _appOptions.CSRFCookieName) - { - var val = pairs[1]; - httpContext.Response.Cookies.Delete(pairs[0]); - httpContext.Response.Cookies.Append($"{_appOptions.CSRFCookieName}_{port}", val); - } - else if (pairs[0].Trim() == HttpExtensions.REFRESH_TOKEN_COOKIE_KEY) - { - var val = pairs[1]; - httpContext.Response.Cookies.Delete(pairs[0]); - httpContext.Response.Cookies.Append($"{HttpExtensions.REFRESH_TOKEN_COOKIE_KEY}_{port}", val); - } - } - } - } - } - } - } - - private StringValues ModifyCookie(StringValues cookie, int port) - { - List result = []; - var csrfCookieName = $"{_appOptions.CSRFCookieName}_{port}"; - var refreshTokenCookieName = $"{HttpExtensions.REFRESH_TOKEN_COOKIE_KEY}_{port}"; - foreach (var cookieItem in cookie) - { - if (null != cookieItem) - { - StringBuilder itemBuilder = new(); - var cookies = cookieItem.Split(';'); - foreach (var item in cookies) - { - if (null != item) - { - var pairs = item.Split('='); - if (pairs.Length == 2) - { - if (0 != itemBuilder.Length) - itemBuilder.Append(';'); - if (pairs[0].Trim() == csrfCookieName) - { - itemBuilder.Append(_appOptions.CSRFCookieName); - itemBuilder.Append('='); - itemBuilder.Append(pairs[1].Trim()); - } - else if (pairs[0].Trim() == refreshTokenCookieName) - { - itemBuilder.Append(HttpExtensions.REFRESH_TOKEN_COOKIE_KEY); - itemBuilder.Append('='); - itemBuilder.Append(pairs[1].Trim()); - } - else - { - itemBuilder.Append(item); - } - } - else - { - itemBuilder.Append(item); - } - } - } - - result.Add(itemBuilder.ToString()); - } - } - return new StringValues(result.ToArray()); - } -} diff --git a/StopShopping.Api/Middlewares/GlobalExceptionHandlerMiddleware.cs b/StopShopping.Api/Middlewares/GlobalExceptionHandlerMiddleware.cs index 6043340..26f480f 100644 --- a/StopShopping.Api/Middlewares/GlobalExceptionHandlerMiddleware.cs +++ b/StopShopping.Api/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -24,26 +24,32 @@ public class GlobalExceptionHandlerMiddleware try { await _next(httpContext); - } - catch (BadHttpRequestException ex) when (ex.InnerException is AntiforgeryValidationException) - { - var problemDetails = new ProblemDetails + httpContext.Response.OnStarting(async () => { - Detail = ex.InnerException.Message, - Instance = httpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Title = "CSRF 错误", - }; + var antiforgeryFeature = httpContext.Features.Get(); + if (null != antiforgeryFeature && !antiforgeryFeature.IsValid) + { + var problemDetails = new ProblemDetails + { + Detail = antiforgeryFeature.Error?.Message, + Instance = httpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Title = "CSRF 错误", + }; - problemDetails.AddErrorCode(ProblemDetailsCodes.CsrfValidationFailed); + problemDetails.AddErrorCode(ProblemDetailsCodes.CsrfValidationFailed); - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - httpContext.Response.ContentType = "application/problem+json"; + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + httpContext.Response.ContentType = "application/problem+json"; - await _problemDetailsService.WriteAsync(new ProblemDetailsContext - { - HttpContext = httpContext, - ProblemDetails = problemDetails + await _problemDetailsService.WriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, + ProblemDetails = problemDetails + }); + } + + await Task.CompletedTask; }); } catch (BadHttpRequestException ex) diff --git a/StopShopping.Api/Program.cs b/StopShopping.Api/Program.cs index 658718b..82a8724 100644 --- a/StopShopping.Api/Program.cs +++ b/StopShopping.Api/Program.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.HostFiltering; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; using Scalar.AspNetCore; using Serilog; using StopShopping.Api.Extensions; using StopShopping.Api.Routes; -using StopShopping.Api.Workers; const string CORS_POLICY = "default"; // 将启动日志写入控制台,用于捕获启动时异常,启动后WriteTo被后续配置替代 @@ -41,13 +43,11 @@ try }, appConfiguration, builder.Configuration.GetSection("OpenPlatformOptions")); - builder.Services.AddHostedService(); /**********************************************************************/ var app = builder.Build(); if (app.Environment.IsDevelopment()) { - app.UseDevelopmentCookie(); app.MapOpenApi(); app.MapScalarApiReference(options => { @@ -55,18 +55,30 @@ try }); } + if (!app.Environment.IsDevelopment()) + { + app.UseHostFiltering(); + + var forwardedHeadersOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.All, + }; + var hostFilteringOptions = app.Services.GetRequiredService>(); + if (null != hostFilteringOptions) + forwardedHeadersOptions.AllowedHosts = hostFilteringOptions.Value.AllowedHosts; + app.UseForwardedHeaders(forwardedHeadersOptions); + } + app.UseGlobalExceptionHandler(); - app.UseRouting(); - app.UseCors(CORS_POLICY); + app.UseRouting(); + app.UseAuthentication(); app.UseAuthorization(); - app.MapStaticAssets().ShortCircuit(); - Root.MapRoutes(app); app.UseAntiforgery(); diff --git a/StopShopping.Api/Routes/Admin.cs b/StopShopping.Api/Routes/Admin.cs deleted file mode 100644 index dbcfc36..0000000 --- a/StopShopping.Api/Routes/Admin.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Extensions.Options; -using StopShopping.Services; -using StopShopping.Services.Extensions; -using StopShopping.Services.Models.Req; -using StopShopping.Services.Models.Resp; - -namespace StopShopping.Api.Routes; - -public static class Admin -{ - public static RouteGroupBuilder MapAdmin(this RouteGroupBuilder routes) - { - routes.MapPost("/admin/signin", SignInAsync) - .AllowAnonymous().WithTags(OpenApiTags.管理员.ToString()); - - return routes; - } - - private static async Task> SignInAsync( - SignInParams model, - IUserService userService, - HttpContext httpContext, - IWebHostEnvironment env, - IOptions options) - { - var result = await userService.SignInAdminAsync(model); - var resp = new ApiResponse - { - IsSucced = result.IsSucced, - Data = result.User, - Message = result.Message - }; - if (result.IsSucced) - { - httpContext.Response.Cookies.AppendRefreshToken( - env, - options.Value, - TimeSpan.FromSeconds(result.RefreshToken!.ExpiresIn), - result.RefreshToken.Token! - ); - } - return resp; - } -} diff --git a/StopShopping.Api/Routes/Category.cs b/StopShopping.Api/Routes/Category.cs index c6d79bd..dbde386 100644 --- a/StopShopping.Api/Routes/Category.cs +++ b/StopShopping.Api/Routes/Category.cs @@ -1,29 +1,13 @@ using StopShopping.Services; -using StopShopping.Services.Models.Req; using StopShopping.Services.Models.Resp; namespace StopShopping.Api.Routes; public static class Category { - public static RouteGroupBuilder MapCategoryCommon(this RouteGroupBuilder routes) - { - routes.MapGet("/category/list", GetTree) - .WithTags(OpenApiTags.分类.ToString()) - .AllowAnonymous(); - - return routes; - } - public static RouteGroupBuilder MapCategory(this RouteGroupBuilder routes) { - routes.MapPost("/category/edit", EditCategoryAsync) - .WithTags(OpenApiTags.分类.ToString()); - - routes.MapPost("/category/resort", ResortCategoryAsync) - .WithTags(OpenApiTags.分类.ToString()); - - routes.MapPost("/category/delete", DeleteCategoryAsync) + routes.MapGet("/category/list", GetTree) .WithTags(OpenApiTags.分类.ToString()); return routes; @@ -35,26 +19,4 @@ public static class Category { return categoryService.GetCategoriesTree(); } - - private static async Task> EditCategoryAsync( - EditCategoryParams model, - ICategoryService categoryService) - { - return await categoryService.EditCategoryAsync(model); - } - - private static async Task ResortCategoryAsync( - ResortCategoryParams model, - ICategoryService categoryService) - { - return await categoryService.ResortCategoryAsync(model); - } - - private static async Task DeleteCategoryAsync( - CategoryIdParams model, - ICategoryService categoryService - ) - { - return await categoryService.DeleteCategoryAsync(model); - } } diff --git a/StopShopping.Api/Routes/Common.cs b/StopShopping.Api/Routes/Common.cs index 6981023..d1c5267 100644 --- a/StopShopping.Api/Routes/Common.cs +++ b/StopShopping.Api/Routes/Common.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Mvc; -using Microsoft.Net.Http.Headers; using StopShopping.Services; using StopShopping.Services.Models.Req; using StopShopping.Services.Models.Resp; @@ -12,23 +11,15 @@ public static class Common public static RouteGroupBuilder MapCommon(this RouteGroupBuilder routes) { routes.MapPost("/common/upload", UploadAsync) - .WithTags(OpenApiTags.公用.ToString()); - - routes.MapPost("/common/refreshtoken", RefreshTokenAsync) - .AllowAnonymous() - .Produces>() - .WithTags(OpenApiTags.公用.ToString()); - - routes.MapPost("/common/signout", SignOutAsync) - .AllowAnonymous().WithTags(OpenApiTags.公用.ToString()); + .WithTags(OpenApiTags.通用.ToString()); routes.MapPost("/common/antiforgery-token", AntiForgeryToken) - .WithTags(OpenApiTags.公用.ToString()); + .WithTags(OpenApiTags.通用.ToString()); return routes; } - private static async Task> UploadAsync( + private static async Task> UploadAsync( [FromForm] UploadParams payload, IFileService fileService, HttpContext httpContext) @@ -48,40 +39,5 @@ public static class Common HeaderName = antiforgeryToken.HeaderName }); } - private static async Task RefreshTokenAsync( - HttpContext httpContext, - IAccessTokenService accessTokenService) - { - var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; - if (string.IsNullOrWhiteSpace(refreshToken)) - return Results.Unauthorized(); - var accessToken = await accessTokenService.GenerateAccessTokenAsync(refreshToken); - if (null == accessToken) - return Results.Unauthorized(); - - return Results.Ok(new ApiResponse(accessToken)); - } - - public static async Task SignOutAsync( - HttpContext httpContext, - IAccessTokenService accessTokenService) - { - var accessTokenHeader = httpContext.Request.Headers[HeaderNames.Authorization]; - if (accessTokenHeader.Count != 0) - { - var accessToken = accessTokenHeader.First()!.Split(" ").Last(); - if (!string.IsNullOrWhiteSpace(accessToken)) - await accessTokenService.AddAccessTokenBlacklistAsync(accessToken); - } - - var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; - if (!string.IsNullOrWhiteSpace(refreshToken)) - { - await accessTokenService.RevokeRefreshTokenAsync(refreshToken); - httpContext.Response.Cookies.Delete(HttpExtensions.REFRESH_TOKEN_COOKIE_KEY); - } - - return ApiResponse.Succed(); - } } diff --git a/StopShopping.Api/Routes/Product.cs b/StopShopping.Api/Routes/Product.cs index 4458e6d..a2e3940 100644 --- a/StopShopping.Api/Routes/Product.cs +++ b/StopShopping.Api/Routes/Product.cs @@ -26,6 +26,14 @@ public static class Product return routes; } + public static RouteGroupBuilder MapProductAnonymous(this RouteGroupBuilder routes) + { + routes.MapGet("/product/search", SearchProductsAsync) + .WithTags(OpenApiTags.商品.ToString()); + + return routes; + } + private static async Task>> SearchProductsAsync( diff --git a/StopShopping.Api/Routes/Request.cs b/StopShopping.Api/Routes/Request.cs index 57905e9..03ad716 100644 --- a/StopShopping.Api/Routes/Request.cs +++ b/StopShopping.Api/Routes/Request.cs @@ -11,10 +11,6 @@ public static class Request routes.MapPost("/request/publish", PublishRequestAsync) .WithTags(OpenApiTags.需求.ToString()); - routes.MapGet("/request/search", SearchAsync) - .WithTags(OpenApiTags.需求.ToString()) - .AllowAnonymous(); - routes.MapGet("/request/orders", OrderSearchAsync) .WithTags(OpenApiTags.需求.ToString()); @@ -24,6 +20,14 @@ public static class Request return routes; } + public static RouteGroupBuilder MapRequestAnonymous(this RouteGroupBuilder routes) + { + routes.MapGet("/request/search", SearchAsync) + .WithTags(OpenApiTags.需求.ToString()); + + return routes; + } + private static async Task PublishRequestAsync( CreateRequestParams model, IRequestService requestService) diff --git a/StopShopping.Api/Routes/Root.cs b/StopShopping.Api/Routes/Root.cs index d23525b..1483cce 100644 --- a/StopShopping.Api/Routes/Root.cs +++ b/StopShopping.Api/Routes/Root.cs @@ -17,18 +17,16 @@ public static class Root .MapRequest() .MapReply() .MapDistrict() - .WithDescription("用户端调用") + .MapCommon() + .WithDescription("登录用户调用") .RequireAuthorization(policy => policy.RequireRole(SystemRoles.User.ToString())); app.MapGroup("") - .MapCommon() - .MapCategoryCommon() - .WithDescription("公共调用") - .RequireAuthorization(); - app.MapGroup("") - .MapAdmin() + .MapRequestAnonymous() + .MapProductAnonymous() .MapCategory() - .WithDescription("管理端调用") - .RequireAuthorization(policy => policy.RequireRole(SystemRoles.Admin.ToString())); + .MapUserAnonymous() + .WithDescription("匿名用户调用") + .AllowAnonymous(); } } @@ -40,6 +38,5 @@ public enum OpenApiTags 需求, 竞标, 地址, - 管理员, - 公用, + 通用, } diff --git a/StopShopping.Api/Routes/User.cs b/StopShopping.Api/Routes/User.cs index 34c0828..1db371a 100644 --- a/StopShopping.Api/Routes/User.cs +++ b/StopShopping.Api/Routes/User.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; using StopShopping.Services; using StopShopping.Services.Extensions; using StopShopping.Services.Models.Req; @@ -28,6 +29,18 @@ public static class User return routes; } + public static RouteGroupBuilder MapUserAnonymous(this RouteGroupBuilder routes) + { + routes.MapPost("/user/refreshtoken", RefreshTokenAsync) + .Produces>() + .WithTags(OpenApiTags.用户.ToString()); + + routes.MapPost("/user/signout", SignOutAsync) + .WithTags(OpenApiTags.用户.ToString()); + + return routes; + } + private static async Task SignUpAsync( SignUpParams model, IUserService userService) @@ -41,8 +54,8 @@ public static class User SignInParams model, IUserService userService, HttpContext httpContext, - IWebHostEnvironment env, - IOptions options) + IOptions options, + IWebHostEnvironment env) { var result = await userService.SignInAsync(model); var resp = new ApiResponse @@ -73,7 +86,7 @@ public static class User var resp = await userService.ChangePasswordAsync(model); if (resp.IsSucced) - await Common.SignOutAsync(httpContext, accessTokenService); + await SignOutAsync(httpContext, accessTokenService); return resp; } @@ -92,4 +105,41 @@ public static class User { return await userService.EditAsync(model); } + + private static async Task RefreshTokenAsync( + HttpContext httpContext, + IAccessTokenService accessTokenService) + { + var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; + if (string.IsNullOrWhiteSpace(refreshToken)) + return Results.Unauthorized(); + + var accessToken = await accessTokenService.GenerateAccessTokenAsync(refreshToken); + if (null == accessToken) + return Results.Unauthorized(); + + return Results.Ok(new ApiResponse(accessToken)); + } + + public static async Task SignOutAsync( + HttpContext httpContext, + IAccessTokenService accessTokenService) + { + var accessTokenHeader = httpContext.Request.Headers[HeaderNames.Authorization]; + if (accessTokenHeader.Count != 0) + { + var accessToken = accessTokenHeader.First()!.Split(" ").Last(); + if (!string.IsNullOrWhiteSpace(accessToken)) + await accessTokenService.AddAccessTokenBlacklistAsync(accessToken); + } + + var refreshToken = httpContext.Request.Cookies[HttpExtensions.REFRESH_TOKEN_COOKIE_KEY]; + if (!string.IsNullOrWhiteSpace(refreshToken)) + { + await accessTokenService.RevokeRefreshTokenAsync(refreshToken); + httpContext.Response.Cookies.Delete(HttpExtensions.REFRESH_TOKEN_COOKIE_KEY); + } + + return ApiResponse.Succed(); + } } diff --git a/StopShopping.Api/StopShopping.Api.csproj b/StopShopping.Api/StopShopping.Api.csproj index 8c2d8f3..b593c72 100644 --- a/StopShopping.Api/StopShopping.Api.csproj +++ b/StopShopping.Api/StopShopping.Api.csproj @@ -17,10 +17,4 @@ - - - Never - - - diff --git a/StopShopping.Api/appsettings.Template.json b/StopShopping.Api/appsettings.Template.json index 338ffe0..1fe8d61 100644 --- a/StopShopping.Api/appsettings.Template.json +++ b/StopShopping.Api/appsettings.Template.json @@ -10,8 +10,9 @@ "RefreshTokenExpiresIn": "604800" }, "AppOptions": { + "FileApiDomain": "FILE_API_DOMAIN", + "FileApiLocalDomain": "FILE_API_LOCAL_DOMAIN", "CookieDomain": ".example.com或者localhost(开发环境)", - "DomainPath": "https://example.com或者http://localhost(开发环境)", "CSRFHeaderName": "X-CSRF-TOKEN", "CSRFCookieName": "csrf_token", "CorsAllowedOrigins": ["https://web.example.com(跨域)"] diff --git a/StopShopping.FileApi/Extensions/EnumOpenApiSchemaTransformer.cs b/StopShopping.FileApi/Extensions/EnumOpenApiSchemaTransformer.cs new file mode 100644 index 0000000..dab1224 --- /dev/null +++ b/StopShopping.FileApi/Extensions/EnumOpenApiSchemaTransformer.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace StopShopping.FileApi.Extensions; + +/// +/// 处理enum类型openapi显示 +/// +public class EnumOpenApiSchemaTransformer : IOpenApiSchemaTransformer +{ + public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) + { + if (context.JsonTypeInfo.Type.IsEnum) + { + schema.Type = JsonSchemaType.Integer; + + var enumValues = Enum.GetValues(context.JsonTypeInfo.Type) + .Cast() + .Select(v => JsonNode.Parse(Convert.ToInt32(v).ToString())!) + .ToList(); + + schema.Enum = enumValues; + + var enumNames = Enum.GetNames(context.JsonTypeInfo.Type); + schema.Extensions ??= new Dictionary(); + var namesExtension = new JsonNodeExtension(new JsonArray( + enumNames + .Select(n => (JsonNode)n) + .ToArray())); + schema.Extensions.Add("x-enumNames", namesExtension); + + var descMap = new JsonObject(); + foreach (var name in enumNames) + { + if (context.JsonTypeInfo.Type.GetField(name) + ?.GetCustomAttributes(typeof(DescriptionAttribute), false) + .FirstOrDefault() is DescriptionAttribute attr) + { + descMap[name] = attr.Description; + } + } + schema.Extensions.Add("x-enumDescriptions", new JsonNodeExtension(descMap)); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/StopShopping.FileApi/Extensions/MiddlewareExtensions.cs b/StopShopping.FileApi/Extensions/MiddlewareExtensions.cs new file mode 100644 index 0000000..c556ec7 --- /dev/null +++ b/StopShopping.FileApi/Extensions/MiddlewareExtensions.cs @@ -0,0 +1,13 @@ +using StopShopping.FileApi.Middlewares; + +namespace StopShopping.FileApi.Extensions; + +public static class MiddlewareExtensions +{ + public static IApplicationBuilder UseInternalOnlyAccess(this IApplicationBuilder applicationBuilder) + { + applicationBuilder.UseMiddleware(); + + return applicationBuilder; + } +} diff --git a/StopShopping.FileApi/Extensions/RouteGroupBuilderExtensions.cs b/StopShopping.FileApi/Extensions/RouteGroupBuilderExtensions.cs new file mode 100644 index 0000000..c969268 --- /dev/null +++ b/StopShopping.FileApi/Extensions/RouteGroupBuilderExtensions.cs @@ -0,0 +1,13 @@ +namespace StopShopping.FileApi.Extensions; + +public static class RouteGroupBuilderExtensions +{ + public static RouteGroupBuilder WithInternalOnly(this RouteGroupBuilder routes) + { + routes.WithMetadata(new InternalOnlyMetadata()); + + return routes; + } +} + +public class InternalOnlyMetadata { } diff --git a/StopShopping.FileApi/Middlewares/InternalAccessOnlyMiddleware.cs b/StopShopping.FileApi/Middlewares/InternalAccessOnlyMiddleware.cs new file mode 100644 index 0000000..6376887 --- /dev/null +++ b/StopShopping.FileApi/Middlewares/InternalAccessOnlyMiddleware.cs @@ -0,0 +1,58 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; +using StopShopping.FileApi.Extensions; + +namespace StopShopping.FileApi.Middlewares; + +public class InternalAccessOnlyMiddleware +{ + public InternalAccessOnlyMiddleware( + RequestDelegate next, + IProblemDetailsService problemDetailsService, + ILogger logger) + { + _next = next; + _problemService = problemDetailsService; + _logger = logger; + } + + private readonly RequestDelegate _next; + private readonly IProblemDetailsService _problemService; + private readonly ILogger _logger; + + public async Task InvokeAsync(HttpContext httpContext) + { + var endpoint = httpContext.GetEndpoint(); + if (null != endpoint) + { + var internalOnlyMetadata = endpoint.Metadata.GetMetadata(); + if (null != internalOnlyMetadata) + { + if (null == httpContext.Connection.RemoteIpAddress + || !IPAddress.IsLoopback(httpContext.Connection.RemoteIpAddress)) + { + var problemDetails = new ProblemDetails + { + Detail = $"remote ip: {httpContext.Connection.RemoteIpAddress}", + Instance = httpContext.Request.Path, + Status = StatusCodes.Status403Forbidden, + Title = "access denied, local access only." + }; + + httpContext.Response.StatusCode = StatusCodes.Status403Forbidden; + httpContext.Response.ContentType = "application/problem+json"; + + await _problemService.WriteAsync(new ProblemDetailsContext + { + HttpContext = httpContext, + ProblemDetails = problemDetails, + }); + + _logger.LogInformation("denied access: {Ip}", httpContext.Connection.RemoteIpAddress); + } + } + } + + await _next(httpContext); + } +} diff --git a/StopShopping.FileApi/Program.cs b/StopShopping.FileApi/Program.cs new file mode 100644 index 0000000..38dcbce --- /dev/null +++ b/StopShopping.FileApi/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.HostFiltering; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Options; +using Scalar.AspNetCore; +using Serilog; +using StopShopping.FileApi; +using StopShopping.FileApi.Extensions; +using StopShopping.FileApi.Services; + +// 将启动日志写入控制台,用于捕获启动时异常,启动后WriteTo被后续配置替代 +Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateBootstrapLogger(); + +try +{ + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddSerilog((services, seriLogger) => + { + seriLogger.ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services); + }); + + builder.Services.AddOpenApi(options => + { + options.AddSchemaTransformer(); + }); + builder.Services.AddProblemDetails(); + builder.Services.AddServices(builder.Configuration.GetSection("AppOptions")); + /********************************************************************/ + var app = builder.Build(); + + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + app.MapScalarApiReference(); + } + + if (!app.Environment.IsDevelopment()) + { + app.UseHostFiltering(); + + var forwardedHeadersOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.All, + }; + var hostFilteringOptions = app.Services.GetRequiredService>(); + if (null != hostFilteringOptions) + forwardedHeadersOptions.AllowedHosts = hostFilteringOptions.Value.AllowedHosts; + app.UseForwardedHeaders(forwardedHeadersOptions); + } + + app.UseStaticFiles(); + + Routes.MapRoutes(app); + + app.UseInternalOnlyAccess(); + + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "启动异常!"); +} +finally +{ + Log.CloseAndFlush(); +} diff --git a/StopShopping.FileApi/Properties/launchSettings.json b/StopShopping.FileApi/Properties/launchSettings.json new file mode 100644 index 0000000..ba065c9 --- /dev/null +++ b/StopShopping.FileApi/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7178;http://localhost:5072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/StopShopping.FileApi/Routes.cs b/StopShopping.FileApi/Routes.cs new file mode 100644 index 0000000..715ab26 --- /dev/null +++ b/StopShopping.FileApi/Routes.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using StopShopping.FileApi.Extensions; +using StopShopping.FileApi.Services; + +namespace StopShopping.FileApi; + +public static class Routes +{ + public static void MapRoutes(WebApplication app) + { + app.MapGroup("") + .MapRoutes() + .WithInternalOnly(); + } + + public static RouteGroupBuilder MapRoutes(this RouteGroupBuilder routes) + { + routes.MapPost("/upload", UploadAsync) + .DisableAntiforgery() + .WithDescription("上传文件,对外接口自己实现anti-forgery(重要)"); + + return routes; + } + + private static async Task> UploadAsync( + [FromForm] UploadParams payload, + IFileService fileService) + { + return await fileService.UploadFileAsync(payload); + } +} diff --git a/StopShopping.FileApi/Services/ApiResponse.cs b/StopShopping.FileApi/Services/ApiResponse.cs new file mode 100644 index 0000000..9755adc --- /dev/null +++ b/StopShopping.FileApi/Services/ApiResponse.cs @@ -0,0 +1,64 @@ +namespace StopShopping.FileApi.Services; + +/// +/// 强类型返回值 +/// +/// +public class ApiResponse +{ + public ApiResponse() + { } + + public ApiResponse(T data) + { + Data = data; + } + + /// + /// 是否成功 + /// + /// + public bool IsSucced { get; set; } = true; + + /// + /// 错误消息 + /// + /// + public string? Message { get; set; } + + /// + /// 关联数据 + /// + /// + public T? Data { get; set; } + + public ApiResponse Failed(string message) + { + IsSucced = false; + Message = message; + + return this; + } +} + +/// +/// 强类型返回值,只返回成功与否和消息 +/// +public class ApiResponse : ApiResponse +{ + public ApiResponse(bool isSucced = true, string message = "") + { + IsSucced = isSucced; + Message = message; + } + + public static ApiResponse Succed(string message = "") + { + return new ApiResponse(message: message); + } + + public static new ApiResponse Failed(string message) + { + return new ApiResponse(false, message); + } +} diff --git a/StopShopping.FileApi/Services/AppOptions.cs b/StopShopping.FileApi/Services/AppOptions.cs new file mode 100644 index 0000000..d9fc7a3 --- /dev/null +++ b/StopShopping.FileApi/Services/AppOptions.cs @@ -0,0 +1,6 @@ +namespace StopShopping.FileApi.Services; + +public class AppOptions +{ + public string Domain { get; set; } = string.Empty; +} diff --git a/StopShopping.FileApi/Services/Extensions.cs b/StopShopping.FileApi/Services/Extensions.cs new file mode 100644 index 0000000..d2e0f61 --- /dev/null +++ b/StopShopping.FileApi/Services/Extensions.cs @@ -0,0 +1,34 @@ + +using FileSignatures; +using FileSignatures.Formats; +using StopShopping.FileApi.Services.Implementions; + +namespace StopShopping.FileApi.Services; + +public static class Extensions +{ + public static IServiceCollection AddServices(this IServiceCollection services, IConfiguration appOptions) + { + services.Configure(appOptions); + services.AddValidation(); + + var imageFormats = FileFormatLocator.GetFormats().OfType(); + var imageInspector = new FileFormatInspector(imageFormats); + services.AddSingleton(imageInspector); + + services.AddScoped(); + + return services; + } + + public static string GetTargetDirectory(this UploadScences uploadScences) + { + return uploadScences switch + { + UploadScences.Avatar => "avatar", + UploadScences.Product => "product", + UploadScences.Category => "category", + _ => throw new ArgumentOutOfRangeException(nameof(uploadScences)) + }; + } +} \ No newline at end of file diff --git a/StopShopping.FileApi/Services/FileUploadResp.cs b/StopShopping.FileApi/Services/FileUploadResp.cs new file mode 100644 index 0000000..58ca5f2 --- /dev/null +++ b/StopShopping.FileApi/Services/FileUploadResp.cs @@ -0,0 +1,18 @@ +namespace StopShopping.FileApi.Services; + +/// +/// 文件上传结果 +/// +public class FileUploadResp +{ + /// + /// 新名,上传后重命名 + /// + /// + public string NewName { get; set; } = string.Empty; + /// + /// Url + /// + /// + public string Url { get; set; } = string.Empty; +} diff --git a/StopShopping.FileApi/Services/IFileService.cs b/StopShopping.FileApi/Services/IFileService.cs new file mode 100644 index 0000000..c16a00e --- /dev/null +++ b/StopShopping.FileApi/Services/IFileService.cs @@ -0,0 +1,14 @@ +namespace StopShopping.FileApi.Services; + +/// +/// 文件服务 +/// +public interface IFileService +{ + /// + /// 上传文件 + /// + /// + /// + Task> UploadFileAsync(UploadParams payload); +} diff --git a/StopShopping.FileApi/Services/Implementions/FileService.cs b/StopShopping.FileApi/Services/Implementions/FileService.cs new file mode 100644 index 0000000..53c6099 --- /dev/null +++ b/StopShopping.FileApi/Services/Implementions/FileService.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Options; + +namespace StopShopping.FileApi.Services.Implementions; + +public class FileService : IFileService +{ + public FileService(IOptions appOptions, + IWebHostEnvironment webHostEnvironment) + { + _appOptions = appOptions.Value; + _env = webHostEnvironment; + } + + private readonly IWebHostEnvironment _env; + private readonly AppOptions _appOptions; + + public async Task> UploadFileAsync(UploadParams payload) + { + var newName = Guid.NewGuid().ToString("N").ToLower(); + var extension = Path.GetExtension(payload.File!.FileName); + var newFullName = $"{newName}{extension}"; + var relativeToRootPath = GetRelativeToRootPath(payload.Scences, newFullName); + var targetPath = Path.Combine(_env.WebRootPath, GetRelativeToRootPath(payload.Scences)); + + if (!Directory.Exists(targetPath)) + { + Directory.CreateDirectory(targetPath); + } + + using var file = new FileStream( + Path.Combine(_env.WebRootPath, relativeToRootPath), + FileMode.CreateNew, + FileAccess.Write); + + await payload.File.CopyToAsync(file); + + FileUploadResp result = new() + { + NewName = newFullName, + Url = GetFileUrl(payload.Scences, newFullName), + }; + + return new ApiResponse(result); + } + + private string GetFileUrl(UploadScences scences, string fileName) + { + return $"{_appOptions.Domain}/{GetRelativeToRootPath(scences, fileName) + .Replace(Path.DirectorySeparatorChar, '/')}"; + } + + private string GetRelativeToRootPath(UploadScences scences, string fileName = "") + { + return Path.Combine( + "images", + scences.GetTargetDirectory(), + fileName); + } +} diff --git a/StopShopping.FileApi/Services/NameUrlParams.cs b/StopShopping.FileApi/Services/NameUrlParams.cs new file mode 100644 index 0000000..1d76c5f --- /dev/null +++ b/StopShopping.FileApi/Services/NameUrlParams.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace StopShopping.FileApi.Services; + +/// +/// url查询参数 +/// +/// +public record NameUrlParams +{ + /// + /// 场景 + /// + /// + public UploadScences Scences { get; set; } + /// + /// 文件名 + /// + /// + [Required] + public string[] Names { get; set; } = []; +} diff --git a/StopShopping.FileApi/Services/NameUrlResp.cs b/StopShopping.FileApi/Services/NameUrlResp.cs new file mode 100644 index 0000000..1b1bb3e --- /dev/null +++ b/StopShopping.FileApi/Services/NameUrlResp.cs @@ -0,0 +1,14 @@ +namespace StopShopping.FileApi.Services; + +/// +/// url查询响应 +/// +/// +public class NameUrlResp +{ + /// + /// 文件名:文件链接 + /// + /// + public KeyValuePair[] NameUrls { get; set; } = []; +} diff --git a/StopShopping.FileApi/Services/UploadParams.cs b/StopShopping.FileApi/Services/UploadParams.cs new file mode 100644 index 0000000..9dc04c7 --- /dev/null +++ b/StopShopping.FileApi/Services/UploadParams.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace StopShopping.FileApi.Services; + +/// +/// 上传 +/// +/// +public record UploadParams +{ + /// + /// 场景 + /// + /// + public UploadScences Scences { get; set; } + /// + /// 文件 + /// + /// + [Required] + [ImageFileValidation(2 * 1024 * 1024)] + public IFormFile? File { get; set; } +} diff --git a/StopShopping.FileApi/Services/UploadScences.cs b/StopShopping.FileApi/Services/UploadScences.cs new file mode 100644 index 0000000..9aa0196 --- /dev/null +++ b/StopShopping.FileApi/Services/UploadScences.cs @@ -0,0 +1,20 @@ +namespace StopShopping.FileApi.Services; + +/// +/// 文件上传场景 +/// +public enum UploadScences +{ + /// + /// 头像 + /// + Avatar, + /// + /// 商品 + /// + Product, + /// + /// 商品分类 + /// + Category, +} diff --git a/StopShopping.Services/Models/Validator/ImageFileValidationAttribute.cs b/StopShopping.FileApi/Services/Validator/ImageFileValidationAttribute.cs similarity index 94% rename from StopShopping.Services/Models/Validator/ImageFileValidationAttribute.cs rename to StopShopping.FileApi/Services/Validator/ImageFileValidationAttribute.cs index f3211a8..81236f0 100644 --- a/StopShopping.Services/Models/Validator/ImageFileValidationAttribute.cs +++ b/StopShopping.FileApi/Services/Validator/ImageFileValidationAttribute.cs @@ -1,6 +1,4 @@ using FileSignatures; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; namespace System.ComponentModel.DataAnnotations; diff --git a/StopShopping.FileApi/StopShopping.FileApi.csproj b/StopShopping.FileApi/StopShopping.FileApi.csproj new file mode 100644 index 0000000..6e83bf6 --- /dev/null +++ b/StopShopping.FileApi/StopShopping.FileApi.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + Never + + + Never + + + + diff --git a/StopShopping.FileApi/appsettings.Template.json b/StopShopping.FileApi/appsettings.Template.json new file mode 100644 index 0000000..f44d317 --- /dev/null +++ b/StopShopping.FileApi/appsettings.Template.json @@ -0,0 +1,26 @@ +{ + "AppOptions": { + "Domain": "DOMAIN" + }, + "Serilog": { + "Using": ["Serilog.Sinks.File"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "./logs/log-.txt", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:HH:mm:ss.fff} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}" + } + } + ] + }, + "AllowedHosts": "*" +} diff --git a/StopShopping.Services/Consts.cs b/StopShopping.Services/Consts.cs index 314d8be..1c18d4c 100644 --- a/StopShopping.Services/Consts.cs +++ b/StopShopping.Services/Consts.cs @@ -19,6 +19,11 @@ public static class Consts /// public const string DEFAULT_ADMIN_ACCOUNT = "stopshopping"; + /// + /// 文件服务httpclient名 + /// + public const string FILE_API_CLIENT_NAME = "file_api_client"; + public static class CacheKeys { public static string AccessTokenBlacklist(string token) diff --git a/StopShopping.Services/Extensions/AppOptions.cs b/StopShopping.Services/Extensions/AppOptions.cs index b09b28d..5e0002b 100644 --- a/StopShopping.Services/Extensions/AppOptions.cs +++ b/StopShopping.Services/Extensions/AppOptions.cs @@ -5,17 +5,22 @@ namespace StopShopping.Services.Extensions; /// public record AppOptions { + /// + /// 文件服务站点 + /// + /// + public string FileApiDomain { get; set; } = string.Empty; + /// + /// 文件服务本地站点 + /// + /// + public string FileApiLocalDomain { get; set; } = string.Empty; /// /// .bjbj.me /// /// public string CookieDomain { get; set; } = string.Empty; /// - /// 域名,http(s)://www.xxx.xx - /// - /// - public string DomainPath { get; set; } = string.Empty; - /// /// anti-forgery 请求头 /// /// diff --git a/StopShopping.Services/Extensions/ServicesExtensions.cs b/StopShopping.Services/Extensions/ServicesExtensions.cs index 0500323..7db3657 100644 --- a/StopShopping.Services/Extensions/ServicesExtensions.cs +++ b/StopShopping.Services/Extensions/ServicesExtensions.cs @@ -1,7 +1,6 @@ -using FileSignatures; -using FileSignatures.Formats; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using StopShopping.EF; using StopShopping.Services; using StopShopping.Services.Extensions; @@ -21,16 +20,18 @@ public static class ServicesExtensions services.Configure(appOptions); - var imageFormats = FileFormatLocator.GetFormats().OfType(); - var imageInspector = new FileFormatInspector(imageFormats); - services.AddSingleton(imageInspector); + services.AddHttpClient(Consts.FILE_API_CLIENT_NAME, (sp, client) => + { + var options = sp.GetRequiredService>(); + client.BaseAddress = new Uri(options.Value.FileApiLocalDomain); + }); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/StopShopping.Services/IClaimsService.cs b/StopShopping.Services/IClaimsService.cs index 472b667..1181bd1 100644 --- a/StopShopping.Services/IClaimsService.cs +++ b/StopShopping.Services/IClaimsService.cs @@ -24,5 +24,5 @@ public interface IClaimsService /// 获取当前登录用户id /// /// - int GetCurrentUserId(); + int? GetCurrentUserId(); } diff --git a/StopShopping.Services/IFileService.cs b/StopShopping.Services/IFileService.cs index 8f83c0e..a3f8aed 100644 --- a/StopShopping.Services/IFileService.cs +++ b/StopShopping.Services/IFileService.cs @@ -14,7 +14,7 @@ public interface IFileService /// /// /// - Task> UploadFileAsync(UploadParams payload); + Task> UploadFileAsync(UploadParams payload); /// /// 获取文件链接 /// diff --git a/StopShopping.Services/Implementions/CategoryService.cs b/StopShopping.Services/Implementions/CategoryService.cs index 3661541..397f143 100644 --- a/StopShopping.Services/Implementions/CategoryService.cs +++ b/StopShopping.Services/Implementions/CategoryService.cs @@ -2,6 +2,7 @@ using System.Data.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using StopShopping.EF; +using StopShopping.Services.Models; using StopShopping.Services.Models.Req; using StopShopping.Services.Models.Resp; @@ -182,8 +183,8 @@ public class CategoryService : ICategoryService { Id = model.Id, LogoUrl = string.IsNullOrWhiteSpace(model.Logo) - ? "" - : _fileService.GetFileUrl(Models.UploadScences.Category, model.Logo), + ? "" + : _fileService.GetFileUrl(UploadScences.Category, model.Logo), Name = model.Name, Order = model.Order, ParentId = model.ParentId, diff --git a/StopShopping.Services/Implementions/ClaimsService.cs b/StopShopping.Services/Implementions/ClaimsService.cs index 21ff91c..c05fef1 100644 --- a/StopShopping.Services/Implementions/ClaimsService.cs +++ b/StopShopping.Services/Implementions/ClaimsService.cs @@ -41,13 +41,13 @@ public class ClaimsService : IClaimsService return claimsIdentity; } - public int GetCurrentUserId() + public int? GetCurrentUserId() { var currUserId = _httpContextAccessor.HttpContext ?.User.FindFirstValue(JwtRegisteredClaimNames.Sub); if (string.IsNullOrWhiteSpace(currUserId)) - throw new InvalidOperationException("在错误的位置获取当前登录用户"); + return null; return Convert.ToInt32(currUserId); } diff --git a/StopShopping.Services/Implementions/FileService.cs b/StopShopping.Services/Implementions/FileService.cs index a4f1223..1c8e041 100644 --- a/StopShopping.Services/Implementions/FileService.cs +++ b/StopShopping.Services/Implementions/FileService.cs @@ -1,4 +1,9 @@ -using Microsoft.AspNetCore.Hosting; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StopShopping.Services.Extensions; using StopShopping.Services.Models; @@ -9,50 +14,66 @@ namespace StopShopping.Services.Implementions; public class FileService : IFileService { - public FileService(IOptions appOptions, - IWebHostEnvironment webHostEnvironment) + public FileService( + IHttpClientFactory httpClientFactory, + IOptions options, + ILogger logger) { - _appOptions = appOptions.Value; - _env = webHostEnvironment; + _fileClient = httpClientFactory.CreateClient(Consts.FILE_API_CLIENT_NAME); + _options = options.Value; + _logger = logger; } - private readonly AppOptions _appOptions; - private readonly IWebHostEnvironment _env; + private readonly HttpClient _fileClient; + private readonly AppOptions _options; + private readonly ILogger _logger; - public async Task> UploadFileAsync(UploadParams payload) + private const string UPLOAD_PATH = "/upload"; + + public async Task> UploadFileAsync(UploadParams payload) { - var newName = Guid.NewGuid().ToString("N").ToLower(); - var extension = Path.GetExtension(payload.File!.FileName); - var newFullName = $"{newName}{extension}"; - var relativeToRootPath = GetRelativeToRootPath(payload.Scences, newFullName); - var targetPath = Path.Combine(_env.WebRootPath, GetRelativeToRootPath(payload.Scences)); - - if (!Directory.Exists(targetPath)) + try { - Directory.CreateDirectory(targetPath); + var formData = new MultipartFormDataContent + { + { new StringContent(((int)payload.Scences).ToString()), nameof(payload.Scences) }, + }; + var fileContent = new StreamContent(payload.File!.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(payload.File.ContentType); + formData.Add(fileContent, nameof(payload.File), payload.File.FileName); + + var resp = await _fileClient.PostAsync(UPLOAD_PATH, formData); + + if (resp.IsSuccessStatusCode) + { + var deserialized = await resp.Content.ReadFromJsonAsync>(); + if (null == deserialized) + throw new JsonException("上传失败"); + return deserialized; + } + else + { + var deserialized = await resp.Content.ReadFromJsonAsync(); + if (null == deserialized) + throw new BadHttpRequestException("上传失败", (int)resp.StatusCode); + throw new BadHttpRequestException( + deserialized.Title ?? deserialized.Detail ?? "上传失败", + deserialized.Status ?? StatusCodes.Status400BadRequest); + } } - - using var file = new FileStream( - Path.Combine(_env.WebRootPath, relativeToRootPath), - FileMode.CreateNew, - FileAccess.Write); - - await payload.File.CopyToAsync(file); - - FileUpload result = new() + catch (Exception e) { - NewName = newFullName, - Url = GetFileUrl(payload.Scences, newFullName) - }; - - return new ApiResponse(result); + _logger.LogError(e, "上传失败"); + throw; + } } public string GetFileUrl(UploadScences scences, string fileName) { - var relativeToRootPath = GetRelativeToRootPath(scences, fileName); - - return $"{_appOptions.DomainPath}/{relativeToRootPath.Replace(Path.DirectorySeparatorChar, '/')}"; + return string.IsNullOrWhiteSpace(fileName) + ? "" + : $"{_options.FileApiDomain}/{GetRelativeToRootPath(scences, fileName) + .Replace(Path.DirectorySeparatorChar, '/')}"; } private string GetRelativeToRootPath(UploadScences scences, string fileName = "") diff --git a/StopShopping.Services/Implementions/ProductService.cs b/StopShopping.Services/Implementions/ProductService.cs index bf9dbe1..b67921a 100644 --- a/StopShopping.Services/Implementions/ProductService.cs +++ b/StopShopping.Services/Implementions/ProductService.cs @@ -44,7 +44,9 @@ public class ProductService : IProductService var qry = _dbContext.Products .AsNoTracking() .Include(p => p.Category) - .Where(p => !p.Deleted && p.UserId == userId); + .Where(p => !p.Deleted); + if (userId.HasValue) + qry = qry.Where(p => p.UserId == userId); if (model.CategoryId > 0) { var categoryPath = (await _dbContext.Categories @@ -74,7 +76,8 @@ public class ProductService : IProductService public async Task EditAsync(EditProductParams model) { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; + EF.Models.Product? product = null; if (model.Id > 0) { @@ -112,7 +115,7 @@ public class ProductService : IProductService public async Task DeleteAsync(ProductIdParams model) { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; var product = await _dbContext.Products .FirstOrDefaultAsync(p => p.Id == model.ProductId @@ -169,11 +172,11 @@ public class ProductService : IProductService CreateTime = product.CreateTime.ToFormatted(), Description = product.Description, Id = product.Id, + LogoUrl = _fileService.GetFileUrl(Models.UploadScences.Product, product.Logo), MinimumUnit = product.MinimumUnit, Name = product.Name, UnitPrice = product.UnitPrice, SoldAmount = product.SoldAmount, - LogoUrl = _fileService.GetFileUrl(Models.UploadScences.Product, product.Logo) }; if (result is ProductInfo) { @@ -184,10 +187,5 @@ public class ProductService : IProductService return result; } - private string BuildCategoryPath(int categoryId) - { - return $"/{categoryId}/"; - } - #endregion } \ No newline at end of file diff --git a/StopShopping.Services/Implementions/ReplyService.cs b/StopShopping.Services/Implementions/ReplyService.cs index ad79b64..c2455e0 100644 --- a/StopShopping.Services/Implementions/ReplyService.cs +++ b/StopShopping.Services/Implementions/ReplyService.cs @@ -58,7 +58,7 @@ public class ReplyService : IReplyService public async Task ReplyAsync(ReplyParams model) { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; using var trans = await _dbContext.Database.BeginTransactionAsync(); try diff --git a/StopShopping.Services/Implementions/RequestService.cs b/StopShopping.Services/Implementions/RequestService.cs index 59e4972..f3cb0b9 100644 --- a/StopShopping.Services/Implementions/RequestService.cs +++ b/StopShopping.Services/Implementions/RequestService.cs @@ -26,7 +26,7 @@ public class RequestService : IRequestService public async Task PublishRequestAsync(CreateRequestParams model) { var serialNo = _serialNoGenerator.GenerateRequestNo(); - var userId = _claimService.GetCurrentUserId(); + var userId = _claimService.GetCurrentUserId()!.Value; EF.Models.Request req = new() { @@ -62,7 +62,7 @@ public class RequestService : IRequestService public async Task DeleteRequestAsync(RequestIdParams model) { - var userId = _claimService.GetCurrentUserId(); + var userId = _claimService.GetCurrentUserId()!.Value; var request = await _dbContext.Requests .Include(r => r.Replies) @@ -126,7 +126,7 @@ public class RequestService : IRequestService if (userRoles.HasValue) { - var userId = _claimService.GetCurrentUserId(); + var userId = _claimService.GetCurrentUserId()!.Value; qry = userRoles.Value switch { UserRoles.Seller => qry.Where(q => q.Replies.Any(r => r.UserId == userId)), diff --git a/StopShopping.Services/Implementions/UserService.cs b/StopShopping.Services/Implementions/UserService.cs index 77cb42c..a103591 100644 --- a/StopShopping.Services/Implementions/UserService.cs +++ b/StopShopping.Services/Implementions/UserService.cs @@ -150,7 +150,7 @@ public class UserService : IUserService public async Task ChangePasswordAsync(ChangePasswordParams model) { - int userId = _claimsService.GetCurrentUserId(); + int userId = _claimsService.GetCurrentUserId()!.Value; var user = await _dbContext.Users .FirstAsync(u => u.Id == userId); @@ -166,7 +166,7 @@ public class UserService : IUserService public async Task> GetUserInfoAsync() { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; var model = await _dbContext.Users .FirstAsync(u => u.Id == userId); @@ -187,7 +187,7 @@ public class UserService : IUserService public async Task EditAsync(EditUserParams model) { - int userId = _claimsService.GetCurrentUserId(); + int userId = _claimsService.GetCurrentUserId()!.Value; var user = await _dbContext.Users.FirstAsync(u => u.Id == userId); if (!string.IsNullOrWhiteSpace(model.AvatarFileName)) @@ -205,7 +205,7 @@ public class UserService : IUserService public ApiResponse> GetAddresses() { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; var addresses = _dbContext.Addresses .Where(a => a.UserId == userId) @@ -221,7 +221,7 @@ public class UserService : IUserService { EF.Models.Address? address = null; - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; if (model.Id.HasValue && model.Id > 0) { @@ -253,7 +253,7 @@ public class UserService : IUserService public async Task DeleteAddressAsync(int id) { - var userId = _claimsService.GetCurrentUserId(); + var userId = _claimsService.GetCurrentUserId()!.Value; await _dbContext.Addresses .Where(a => a.Id == id && a.UserId == userId) .ExecuteDeleteAsync(); diff --git a/StopShopping.Services/Models/Req/NameUrlParams.cs b/StopShopping.Services/Models/Req/NameUrlParams.cs new file mode 100644 index 0000000..c1de552 --- /dev/null +++ b/StopShopping.Services/Models/Req/NameUrlParams.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace StopShopping.Services.Models.Req; + +/// +/// url查询参数 +/// +/// +public record NameUrlParams +{ + /// + /// 场景 + /// + /// + public UploadScences Scences { get; set; } + /// + /// 文件名 + /// + /// + [Required] + public string[] Names { get; set; } = []; +} diff --git a/StopShopping.Services/Models/Req/UploadParams.cs b/StopShopping.Services/Models/Req/UploadParams.cs index 2ff42cd..84a60b7 100644 --- a/StopShopping.Services/Models/Req/UploadParams.cs +++ b/StopShopping.Services/Models/Req/UploadParams.cs @@ -15,10 +15,9 @@ public record UploadParams /// public UploadScences Scences { get; set; } /// - /// 文件 + /// 文件,2m,图片格式 /// /// [Required] - [ImageFileValidation(2 * 1024 * 1024)] public IFormFile? File { get; set; } } diff --git a/StopShopping.Services/Models/Resp/FileUpload.cs b/StopShopping.Services/Models/Resp/FileUploadResp.cs similarity index 93% rename from StopShopping.Services/Models/Resp/FileUpload.cs rename to StopShopping.Services/Models/Resp/FileUploadResp.cs index d266437..967ce0d 100644 --- a/StopShopping.Services/Models/Resp/FileUpload.cs +++ b/StopShopping.Services/Models/Resp/FileUploadResp.cs @@ -3,7 +3,7 @@ namespace StopShopping.Services.Models.Resp; /// /// 文件上传结果 /// -public class FileUpload +public class FileUploadResp { /// /// 新名,上传后重命名 diff --git a/StopShopping.Services/Models/Resp/NameUrlResp.cs b/StopShopping.Services/Models/Resp/NameUrlResp.cs new file mode 100644 index 0000000..2dc923f --- /dev/null +++ b/StopShopping.Services/Models/Resp/NameUrlResp.cs @@ -0,0 +1,14 @@ +namespace StopShopping.Services.Models.Resp; + +/// +/// url查询响应 +/// +/// +public class NameUrlResp +{ + /// + /// 文件名:文件链接 + /// + /// + public KeyValuePair[] NameUrls { get; set; } = []; +} diff --git a/StopShopping.Services/StopShopping.Services.csproj b/StopShopping.Services/StopShopping.Services.csproj index c769b01..5180b91 100644 --- a/StopShopping.Services/StopShopping.Services.csproj +++ b/StopShopping.Services/StopShopping.Services.csproj @@ -10,7 +10,6 @@ - diff --git a/StopShopping.slnx b/StopShopping.slnx index 877a543..5135d6e 100644 --- a/StopShopping.slnx +++ b/StopShopping.slnx @@ -1,7 +1,9 @@ + +