Files
Yi.Framework/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
chenchun b7756e2112 feat: 新增功能
- 概要
  - 重构并扩展公告相关模型、DTO、服务,新增公告类型、图片与时间字段,调整缓存与查询处理。
  - 新增枚举 AnnouncementTypeEnum。

- 主要改动(简要)
  - Yi.Framework.AiHub.Application.Contracts/Dtos/Announcement/AnnouncementLogDto.cs
    - 新增 ImageUrl、StartTime、EndTime、Type 字段,移除 Date 字段,Title 不再默认空串。
  - Yi.Framework.AiHub.Domain/Entities
    - 重命名 AnnouncementLogAggregateRoot -> AnnouncementAggregateRoot
    - 表名由 Ai_AnnouncementLog 改为 Ai_Announcement(SugarTable 标注)
    - 新增 ImageUrl、StartTime、EndTime、Type、Remark 字段(Remark 已存在,保持)
  - Yi.Framework.AiHub.Domain.Shared/Enums/AnnouncementTypeEnum.cs
    - 新增枚举文件(Activity=1, System=2)
  - Yi.Framework.AiHub.Application.Contracts/IServices/IAnnouncementService.cs
    - GetAsync 返回类型由 AnnouncementOutput 改为 List<AnnouncementLogDto>
  - Yi.Framework.AiHub.Application/Services/AnnouncementService.cs
    - 使用 Mapster 进行 DTO 映射
    - 查询按 StartTime 降序,返回 List<AnnouncementLogDto>,缓存结构简化
  - Yi.Abp.Web/YiAbpWebModule.cs
    - 改为初始化 AnnouncementAggregateRoot 的表(Ai_Announcement)
  - Yi.Ai.Vue3/types/import_meta.d.ts
    - 移除 VITE_BUILD_COMPRESS 环境变量声明

- 重要注意/兼容性提示
  - 接口变更:IAnnouncementService.GetAsync 返回类型已改变,调用方需同步更新(之前返回 AnnouncementOutput 的代码需调整)。
  - 数据库表变更:表名从 Ai_AnnouncementLog -> Ai_Announcement,若需保留历史数据,请在部署前做好数据迁移(重命名表或迁移数据到新表结构),或使用 CodeFirst 初始化新表(当前代码在启动时会 InitTables<AnnouncementAggregateRoot>())。
  - 新增 Mapster 适配(确保项目有 Mapster 依赖)。
  - 前端类型声明移除环境变量后,前端构建/运行脚本若依赖 VITE_BUILD_COMPRESS 需同步调整。
  - 若有缓存结构(AnnouncementCacheDto)或序列化相关约定变更,确认兼容性。

- 建议操作
  - 更新所有使用 IAnnouncementService 的代码(API 层/前端适配返回结构)。
  - 在非生产环境先执行数据迁移验证(保留旧表数据或写迁移脚本)。
  - 确认 Mapster 包已安装并编译通过。
  - 前端项目检查并同步 import_meta.d.ts 变更。
2025-11-10 15:03:02 +08:00

429 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading.RateLimiting;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.Redis.StackExchange;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using StackExchange.Redis;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.Authentication.JwtBearer;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.AntiForgery;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Auditing;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.Caching;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Swashbuckle;
using Yi.Abp.Application;
using Yi.Abp.SqlsugarCore;
using Yi.Framework.AiHub.Application;
using Yi.Framework.AiHub.Application.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AspNetCore;
using Yi.Framework.AspNetCore.Authentication.OAuth;
using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee;
using Yi.Framework.AspNetCore.Authentication.OAuth.QQ;
using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Builder;
using Yi.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection;
using Yi.Framework.AspNetCore.UnifyResult;
using Yi.Framework.BackgroundWorkers.Hangfire;
using Yi.Framework.Bbs.Application;
using Yi.Framework.Bbs.Application.Extensions;
using Yi.Framework.ChatHub.Application;
using Yi.Framework.CodeGen.Application;
using Yi.Framework.Core.Json;
using Yi.Framework.DigitalCollectibles.Application;
using Yi.Framework.Rbac.Application;
using Yi.Framework.Rbac.Domain.Authorization;
using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.Rbac.Domain.Shared.Options;
using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.Stock.Application;
using Yi.Framework.TenantManagement.Application;
namespace Yi.Abp.Web
{
[DependsOn(
typeof(YiAbpSqlSugarCoreModule),
typeof(YiAbpApplicationModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpSwashbuckleModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpAuditingModule),
typeof(AbpAspNetCoreAuthenticationJwtBearerModule),
typeof(YiFrameworkAspNetCoreModule),
typeof(YiFrameworkAspNetCoreAuthenticationOAuthModule),
typeof(YiFrameworkBackgroundWorkersHangfireModule),
typeof(AbpAutofacModule)
)]
public class YiAbpWebModule : AbpModule
{
private const string DefaultCorsPolicyName = "Default";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
//动态Api-改进在pre中配置启动更快
PreConfigure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(YiAbpApplicationModule).Assembly,
option => option.RemoteServiceName = "default");
options.ConventionalControllers.Create(typeof(YiFrameworkRbacApplicationModule).Assembly,
option => option.RemoteServiceName = "rbac");
options.ConventionalControllers.Create(typeof(YiFrameworkBbsApplicationModule).Assembly,
option => option.RemoteServiceName = "bbs");
options.ConventionalControllers.Create(typeof(YiFrameworkChatHubApplicationModule).Assembly,
option => option.RemoteServiceName = "chat-hub");
options.ConventionalControllers.Create(typeof(YiFrameworkTenantManagementApplicationModule).Assembly,
option => option.RemoteServiceName = "tenant-management");
options.ConventionalControllers.Create(typeof(YiFrameworkCodeGenApplicationModule).Assembly,
option => option.RemoteServiceName = "code-gen");
options.ConventionalControllers.Create(typeof(YiFrameworkDigitalCollectiblesApplicationModule).Assembly,
option => option.RemoteServiceName = "digital-collectibles");
options.ConventionalControllers.Create(typeof(YiFrameworkStockApplicationModule).Assembly,
option => option.RemoteServiceName = "ai-stock");
options.ConventionalControllers.Create(typeof(YiFrameworkAiHubApplicationModule).Assembly,
option => option.RemoteServiceName = "ai-hub");
//统一前缀
options.ConventionalControllers.ConventionalControllerSettings.ForEach(x => x.RootPath = "api/app");
});
}
public override Task ConfigureServicesAsync(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var host = context.Services.GetHostingEnvironment();
var service = context.Services;
//本地开发环境,可以禁用作业执行
if (host.IsDevelopment())
{
Configure<AbpBackgroundWorkerOptions>(options => { options.IsEnabled = false; });
}
//请求日志
Configure<AbpAuditingOptions>(options =>
{
//默认关闭,开启会有大量的审计日志
options.IsEnabled = false;
});
//忽略审计日志路径
Configure<AbpAspNetCoreAuditingOptions>(options =>
{
options.IgnoredUrls.Add("/api/app/file/");
options.IgnoredUrls.Add("/hangfire");
});
//采用furion格式的规范化api默认不开启使用abp优雅的方式
//前置需要将管道工作单元前加上app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false);
//你没看错。。。
//service.AddFurionUnifyResultApi();
//配置错误处理显示详情
Configure<AbpExceptionHandlingOptions>(options => { options.SendExceptionsDetailsToClients = true; });
//【NewtonsoftJson严重问题逆天】设置api格式留给后人铭记
// service.AddControllers().AddNewtonsoftJson(options =>
// {
// options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
// options.SerializerSettings.Converters.Add(new StringEnumConverter());
// });
//请使用微软的注意abp date又包了一层采用DefaultJsonTypeInfoResolver统一覆盖
Configure<JsonOptions>(options =>
{
options.JsonSerializerOptions.TypeInfoResolver = new DefaultJsonTypeInfoResolver();
options.JsonSerializerOptions.Converters.Add(new DatetimeJsonConverter());
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
//设置缓存不要过期默认滑动20分钟
Configure<AbpDistributedCacheOptions>(cacheOptions =>
{
cacheOptions.GlobalCacheEntryOptions.SlidingExpiration = null;
//缓存key前缀
cacheOptions.KeyPrefix = "Yi:";
});
Configure<AbpAntiForgeryOptions>(options => { options.AutoValidate = false; });
//Swagger
context.Services.AddYiSwaggerGen<YiAbpWebModule>(options =>
{
options.SwaggerDoc("default",
new OpenApiInfo { Title = "Yi.Framework.Abp", Version = "v1", Description = "集大成者" });
});
//跨域
context.Services.AddCors(options =>
{
options.AddPolicy(DefaultCorsPolicyName, builder =>
{
var corsOrigins = configuration["App:CorsOrigins"]!;
builder
.WithAbpExposedHeaders()
.AllowAnyHeader()
.AllowAnyMethod();
if (corsOrigins == "*")
{
builder.AllowAnyOrigin();
}
else
{
builder
.WithOrigins(corsOrigins
.Split(";", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray())
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowCredentials();
}
});
});
//配置多租户
Configure<AbpTenantResolveOptions>(options =>
{
//基于cookie jwt不好用有坑
options.TenantResolvers.Clear();
options.TenantResolvers.Add(new HeaderTenantResolveContributor());
//options.TenantResolvers.Add(new HeaderTenantResolveContributor());
//options.TenantResolvers.Add(new CookieTenantResolveContributor());
//options.TenantResolvers.RemoveAll(x => x.Name == CookieTenantResolveContributor.ContributorName);
});
//配置Hangfire定时任务存储开启redis后优先使用redis
var redisConfiguration = configuration["Redis:Configuration"];
context.Services.AddHangfire(config =>
{
var redisEnabled = configuration.GetSection("Redis").GetValue<bool>("IsEnabled");
if (redisEnabled)
{
var jobDb = configuration.GetSection("Redis").GetValue<int>("JobDb");
config.UseRedisStorage(
ConnectionMultiplexer.Connect(redisConfiguration),
new RedisStorageOptions()
{
Db = jobDb,
InvisibilityTimeout = TimeSpan.FromHours(1), //JOB允许执行1小时
Prefix = "Yi:HangfireJob:"
}).WithJobExpirationTimeout(TimeSpan.FromHours(1));
}
else
{
config.UseMemoryStorage();
}
});
//速率限制
//每60秒限制100个请求滑块添加分6段
service.AddRateLimiter(_ =>
{
_.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
_.OnRejected = (context, _) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.WriteAsync("Too many requests. Please try again later.");
return new ValueTask();
};
//全局使用,链式表达式
_.GlobalLimiter = PartitionedRateLimiter.CreateChained(
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
{
var userAgent = httpContext.Request.Headers.UserAgent.ToString();
return RateLimitPartition.GetSlidingWindowLimiter
(userAgent, _ =>
new SlidingWindowRateLimiterOptions
{
PermitLimit = 1000,
Window = TimeSpan.FromSeconds(60),
SegmentsPerWindow = 6,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
});
}));
});
//jwt鉴权
var jwtOptions = configuration.GetSection(nameof(JwtOptions)).Get<JwtOptions>();
var refreshJwtOptions = configuration.GetSection(nameof(RefreshJwtOptions)).Get<RefreshJwtOptions>();
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.Zero,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SecurityKey))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
//优先Query中获取再去cookies中获取
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken))
{
context.Token = accessToken;
}
else
{
if (context.Request.Cookies.TryGetValue("Token", out var cookiesToken))
{
context.Token = cookiesToken;
}
}
return Task.CompletedTask;
}
};
})
.AddJwtBearer(TokenTypeConst.Refresh, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.Zero,
ValidateIssuerSigningKey = true,
ValidIssuer = refreshJwtOptions.Issuer,
ValidAudience = refreshJwtOptions.Audience,
IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(refreshJwtOptions.SecurityKey))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var refresh_token = context.Request.Headers["refresh_token"];
if (!string.IsNullOrEmpty(refresh_token))
{
context.Token = refresh_token;
return Task.CompletedTask;
}
var refreshToken = context.Request.Query["refresh_token"];
if (!string.IsNullOrEmpty(refreshToken))
{
context.Token = refreshToken;
}
return Task.CompletedTask;
}
};
})
.AddQQ(options => { configuration.GetSection("OAuth:QQ").Bind(options); })
.AddGitee(options => { configuration.GetSection("OAuth:Gitee").Bind(options); });
//授权
context.Services.AddAuthorization();
return Task.CompletedTask;
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
var service = context.ServiceProvider;
var env = context.GetEnvironment();
var app = context.GetApplicationBuilder();
app.UseRouting();
app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AnnouncementAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<CardFlipTaskAggregateRoot>();
//跨域
app.UseCors(DefaultCorsPolicyName);
if (!env.IsDevelopment())
{
//速率限制
app.UseRateLimiter();
}
//无感token先刷新再鉴权
app.UseRefreshToken();
//鉴权
app.UseAuthentication();
//多租户
app.UseMultiTenancy();
//swagger
app.UseYiSwagger();
//流量访问统计,需redis支持否则不生效
app.UseAccessLog();
//请求处理
app.UseApiInfoHandling();
//静态资源
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = "/api/app/wwwroot",
// 可以在这里添加或修改MIME类型映射
ContentTypeProvider = new FileExtensionContentTypeProvider
{
Mappings =
{
[".wxss"] = "text/css"
}
}
});
app.UseDefaultFiles();
app.UseDirectoryBrowser("/api/app/wwwroot");
//app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false);
//工作单元
app.UseUnitOfWork();
//授权
app.UseAuthorization();
//审计日志
app.UseAuditing();
//日志记录
app.UseAbpSerilogEnrichers();
//Hangfire定时任务面板可配置授权意框架支持jwt
app.UseAbpHangfireDashboard("/hangfire",
options =>
{
options.AsyncAuthorization = new[] { new YiTokenAuthorizationFilter(app.ApplicationServices) };
});
//终节点
app.UseConfiguredEndpoints();
}
}
}