This commit is contained in:
2026-03-30 11:07:30 +08:00
parent 2c44b3a4b2
commit d4a8e71733
74 changed files with 1751 additions and 421 deletions

View File

@@ -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>(jwtOptions);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(jwtBearerOptions =>
{
var jwtConfiguration = jwtOptions.Get<JwtOptions>()!;
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<IAccessTokenService>();
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;
}
}

View File

@@ -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<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes[JwtBearerDefaults.AuthenticationScheme] = bearerOpenApiSecurityScheme;
var securityRequirement = new OpenApiSecurityRequirement
{
{
new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme),
new List<string>()
}
};
document.Security ??= [];
document.Security.Add(securityRequirement);
return Task.CompletedTask;
}
}

View File

@@ -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<AppOptions>();
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<BearerOpenApiDocumentTransformer>();
options.AddSchemaTransformer<EnumOpenApiSchemaTransformer>();
});
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<JwtOptions>();
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;
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi;
namespace StopShopping.AdminApi.Extensions;
/// <summary>
/// 处理enum类型openapi显示
/// </summary>
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<object>()
.Select(v => JsonNode.Parse(Convert.ToInt32(v).ToString())!)
.ToList();
schema.Enum = enumValues;
var enumNames = Enum.GetNames(context.JsonTypeInfo.Type);
schema.Extensions ??= new Dictionary<string, IOpenApiExtension>();
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;
}
}

View File

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

View File

@@ -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<GlobalExceptionHandlerMiddleware>();
return applicationBuilder;
}
}

View File

@@ -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<GlobalExceptionHandlerMiddleware> logger,
IProblemDetailsService problemDetailsService)
{
_next = requestDelegate;
_logger = logger;
_problemDetailsService = problemDetailsService;
}
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
private readonly IProblemDetailsService _problemDetailsService;
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
httpContext.Response.OnStarting(async () =>
{
var antiforgeryFeature = httpContext.Features.Get<IAntiforgeryValidationFeature>();
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,
});
}
}
}

View File

@@ -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<string, object?>();
problemDetails.Extensions.Add(CODE_FIELD, (int)code);
return problemDetails;
}
}
public enum ProblemDetailsCodes
{
CsrfValidationFailed = 1000,
ParametersValidationFailed = 1001,
BadParameters = 1002,
ServerError = 1003,
}

View File

@@ -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<DbSeederBackgroundService>();
/**********************************************************************/
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<IOptions<HostFilteringOptions>>();
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();
}

View File

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

View File

@@ -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<ApiResponse<AccessToken>>()
.WithTags(OpenApiTags..ToString());
routes.MapPost("/admin/signout", SignOutAsync)
.WithTags(OpenApiTags..ToString());
return routes;
}
private static async Task<ApiResponse<SignInAdmin>> SignInAsync(
SignInParams model,
IUserService userService,
HttpContext httpContext,
IWebHostEnvironment env,
IOptions<AppOptions> options)
{
var result = await userService.SignInAdminAsync(model);
var resp = new ApiResponse<SignInAdmin>
{
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<IResult> 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>(accessToken));
}
public static async Task<ApiResponse> 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();
}
}

View File

@@ -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<List<Services.Models.Resp.Category>> GetTree(
ICategoryService categoryService
)
{
return categoryService.GetCategoriesTree();
}
private static async Task<ApiResponse<Services.Models.Resp.Category>> EditCategoryAsync(
EditCategoryParams model,
ICategoryService categoryService)
{
return await categoryService.EditCategoryAsync(model);
}
private static async Task<ApiResponse> ResortCategoryAsync(
ResortCategoryParams model,
ICategoryService categoryService)
{
return await categoryService.ResortCategoryAsync(model);
}
private static async Task<ApiResponse> DeleteCategoryAsync(
CategoryIdParams model,
ICategoryService categoryService
)
{
return await categoryService.DeleteCategoryAsync(model);
}
}

View File

@@ -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<ApiResponse<FileUploadResp>> UploadAsync(
[FromForm] UploadParams payload,
IFileService fileService,
HttpContext httpContext)
{
return await fileService.UploadFileAsync(payload);
}
private static ApiResponse<AntiForgeryToken> AntiForgeryToken(
HttpContext httpContext,
IAntiforgery antiforgery)
{
var antiforgeryToken = antiforgery.GetAndStoreTokens(httpContext);
return new ApiResponse<AntiForgeryToken>(new AntiForgeryToken
{
Token = antiforgeryToken.RequestToken,
HeaderName = antiforgeryToken.HeaderName
});
}
}

View File

@@ -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<List<Services.Models.Resp.District>> GetChildrenDistricts(
[AsParameters] DistrictParentIdParams model,
IDistrictService districtService
)
{
return districtService.GetChildren(model);
}
private static async Task<ApiResponse<List<Services.Models.Resp.District>>> GetTop3LevelDistrictsAsync(
IDistrictService districtService
)
{
return await districtService.GetTop3LevelDistrictsAsync();
}
}

View File

@@ -0,0 +1,61 @@
using StopShopping.Services;
using StopShopping.Services.Models.Req;
using StopShopping.Services.Models.Resp;
namespace StopShopping.AdminApi.Routes;
/// <summary>
/// 商品相关路由
/// </summary>
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<ApiResponse<PagedResult<Services.Models.Resp.Product>>>
SearchProductsAsync(
[AsParameters] ProductSearchParms model,
IProductService productService
)
{
return await productService.SearchAsync(model);
}
private static ApiResponse<ProductInfo> Detail(
[AsParameters] ProductIdParams model,
IProductService productService)
{
return productService.Detail(model);
}
private static async Task<ApiResponse> EditAsync(
EditProductParams model,
IProductService productService
)
{
return await productService.EditAsync(model);
}
private static async Task<ApiResponse> DeleteAsync(
ProductIdParams model,
IProductService productService
)
{
return await productService.DeleteAsync(model);
}
}

View File

@@ -0,0 +1,35 @@
using Scalar.AspNetCore;
using StopShopping.Services.Models;
namespace StopShopping.AdminApi.Routes;
/// <summary>
/// 其他路由从RouteGroupBuilder扩展并添加到MapGroup之后
/// </summary>
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
{
,
,
,
,
,
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Scalar.AspNetCore" Version="2.13.5" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StopShopping.Services\StopShopping.Services.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
using StopShopping.Services;
namespace StopShopping.AdminApi.Workers;
public class DbSeederBackgroundService : BackgroundService
{
public DbSeederBackgroundService(IServiceProvider sp)
{
_sp = sp;
}
private readonly IServiceProvider _sp;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _sp.CreateScope();
using var scope1 = _sp.CreateScope();
var districtService = scope.ServiceProvider.GetRequiredService<IDistrictService>();
var userService = scope1.ServiceProvider.GetRequiredService<IUserService>();
var districtTask = districtService.InitialDatabaseAsync(stoppingToken);
var adminTask = userService.GenerateDefaultAdminAsync();
await Task.WhenAll(districtTask, adminTask);
}
}

View File

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