3. 解决方案分层与启动流程
Furion + SqlSugar 的四层结构:Web.Entry(进程入口)→ Web.Core(装配与中间件)→ Application(业务)→ Core(内核),插件作为横向扩展挂在 Application 上。
1. 工程职责与引用关系
┌─────────────────────────────────────────────┐
│ Admin.NET.Web.Entry (唯一可执行工程) │
│ Program.cs / appsettings.json / wwwroot │
└──────────────────┬──────────────────────────┘
│ ProjectReference
┌──────────────────▼──────────────────────────┐
│ Admin.NET.Web.Core (装配层) │
│ Startup.cs[AppStartup(int.MaxValue)] │
│ Component/WebComponent.cs │
│ Component/ApplicationComponent.cs │
│ Handlers/JwtHandler.cs ProjectOptions.cs │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ Admin.NET.Application (业务层) │
│ Startup.cs[AppStartup(100)] │
│ Configuration/*.json Entity/ Service/ │
│ SeedData/ Const/ EventBus/ Mqtt/ │
└──────┬───────────────────────────┬──────────┘
│ │ Plugins
┌────────────────▼──────────────┐ ┌────────▼─────────────────────┐
│ Admin.NET.Core(内核层) │ │ Ai / Document / PaddleOCR │
│ Entity SqlSugar Service │ │ WorkFlow / WorkWeixin ... │
│ Attribute Const Extension │ │ (各自带 Startup.cs) │
│ SeedData Enum Option Utils │ └──────────────────────────────┘
└────────────────────────────────┘| 工程 | 职责 | 引用 |
|---|---|---|
Admin.NET.Web.Entry | 进程入口、Kestrel、静态资源、Dockerfile | → Web.Core |
Admin.NET.Web.Core | 服务注册 + 中间件管线、JWT Handler、Swagger 分组 | → Application |
Admin.NET.Application | 业务实体、业务服务、业务配置 JSON、业务种子数据 | → Core + 5 个插件 |
Admin.NET.Core | 实体基类、SqlSugar 装配、系统服务、特性、常量 | 无项目引用(只依赖 NuGet) |
Plugins/*(13 个) | 横向可选能力(Ai、WorkFlow、DingTalk、GoView…) | → Core |
解决方案共 4 个主工程 +
Plugins目录下 13 个工程(12 个Admin.NET.Plugin.*+WorkflowCore.Persistence.MySQL)+Admin.NET.Test。 新业务代码一律写在Admin.NET.Application,不要改Core;要改内核请继承/重写而非直接编辑。
2. 目录结构
Admin.NET/
├─ Admin.NET.Web.Entry/
│ ├─ Program.cs # 只有一行 Serve.Run(...)
│ ├─ appsettings.json # 只配置 ConfigurationScanDirectories
│ ├─ Controllers/ Views/ wwwroot/
│ └─ ip2region_v4.xdb sensitive-words.txt
│
├─ Admin.NET.Web.Core/
│ ├─ Startup.cs # [AppStartup(int.MaxValue)] 主装配
│ ├─ Component/WebComponent.cs # IWebComponent(构建期)
│ ├─ Component/ApplicationComponent.cs # IApplicationComponent(管线期)
│ ├─ Handlers/JwtHandler.cs
│ └─ ProjectOptions.cs Mcp/
│
├─ Admin.NET.Application/
│ ├─ Configuration/ # App.json Database.json JWT.json Cache.json
│ │ # Limit.json Logging.json Swagger.json Upload.json
│ │ # CodeGen.json Captcha.json ...
│ ├─ Entity/ # 业务实体(Leave.cs Test.cs TestViewSysUser.cs)
│ ├─ Service/ # 业务服务(App/ Test/ LLM/ Workflow/)
│ ├─ SeedData/ Const/ Option/ EventBus/ Mqtt/ Serial/
│ └─ Startup.cs # [AppStartup(100)]
│
└─ Admin.NET.Core/
├─ Entity/ # EntityBase.cs IEntityFilter.cs Sys*.cs
├─ SqlSugar/ # SqlSugarSetup/Repository/Filter/UnitOfWork/Extension
├─ Service/{User,Config,Dict,Const,Role,Org,Menu,...}/
├─ Attribute/ # 29 个特性(校验/种子/建表/脱敏/幂等…)
├─ Const/ # ConfigConst CacheConst SqlSugarConst ClaimConst
├─ SeedData/ Enum/ Option/ Extension/ Utils/ EventBus/ Job/ Logging/
└─ GlobalUsings.cs3. 命名规范
| 类别 | 规范 | 真实示例 |
|---|---|---|
| 实体 | SysXxx(系统)/ Xxx(业务),贴 [SugarTable] | SysUser、SysPos、Leave |
| 分页入参 | PageXxxInput : BasePageInput | PageUserInput、PageConfigInput |
| 新增入参 | AddXxxInput,通常继承实体 | AddUserInput : SysUser、AddConfigInput |
| 更新入参 | UpdateXxxInput : AddXxxInput | UpdateUserInput : AddUserInput |
| 删除入参 | DeleteXxxInput : BaseIdInput | DeleteUserInput、DeleteConfigInput |
| 出参 | XxxOutput,继承实体后做脱敏/扩展 | UserOutput : SysUser |
| 服务 | XxxService : IDynamicApiController, ITransient | SysUserService、SysConfigService |
| 文件位置 | 实体 Entity/、DTO Service/Xxx/Dto/、Service/Xxx/XxxService.cs | Service/User/Dto/UserInput.cs |
| 接口名 | Page / AddXxx / UpdateXxx / DeleteXxx + [ApiDescriptionSettings(Name="Add")] | PageUserInput ↔ Page() |
4. GlobalUsings
Admin.NET.Core/GlobalUsings.cs 是全框架共享的全局 using,写业务代码时不需要再手写这些:
global using Admin.NET.Core.Service;
global using Furion; // App、Oops、JSON、Log
global using Furion.DynamicApiController; // IDynamicApiController、ApiDescriptionSettings
global using Furion.DependencyInjection; // ITransient / IScoped / ISingleton
global using Furion.FriendlyException; // Oops.Oh
global using Furion.DatabaseAccessor; // UnitOfWork
global using Furion.DataValidation; // DataValidation / ValidationTypes
global using Mapster; // Adapt / Adapt<T>()
global using SqlSugar; // SugarTable / SugarColumn / SqlFunc
global using Yitter.IdGenerator; // YitIdHelper 雪花 Id
global using System.ComponentModel.DataAnnotations; // Required / MaxLength / Range
global using System.Linq.Dynamic.Core; // 动态表达式
global using Microsoft.AspNetCore.Mvc; // HttpPost / FromQuery / FromBody
global using Microsoft.Extensions.DependencyInjection;5. 启动流程
5.1 入口(一行)
// Admin.NET.Web.Entry/Program.cs
using Admin.NET.Web.Core;
Serve.Run(RunOptions.Default
.AddWebComponent<WebComponent>() // 构建期组件
.UseComponent<ApplicationComponent>()); // 管线期组件5.2 构建期:WebComponent(IWebComponent.Load(WebApplicationBuilder, ComponentContext))
- 日志过滤:屏蔽
Microsoft.Hosting/Microsoft.AspNetCore前缀,仅Information以上 - Kestrel:
KeepAliveTimeout/RequestHeadersTimeout= 30 分钟,MaxRequestBodySize= 1GB FormOptions.MultipartBodyLengthLimit= 1GB(大文件上传必须)
5.3 管线期:ApplicationComponent(IApplicationComponent.Load(IApplicationBuilder, ...))
扫描所有继承 AppStartup 且含 LoadAppComponent 方法的类,按 [AppStartup(Order)] 降序执行 LoadAppComponent:
var startups = App.EffectiveTypes
.Where(u => typeof(AppStartup).IsAssignableFrom(u) && u.IsClass && !u.IsAbstract && !u.IsGenericType
&& u.GetMethod("LoadAppComponent") != null)
.OrderByDescending(u => !u.IsDefined(typeof(AppStartupAttribute), true)
? 0 : u.GetCustomAttribute<AppStartupAttribute>(true).Order);5.4 时序总览
Program.cs Serve.Run
└─ WebComponent.Load(builder) # 日志/Kestrel/Form
└─ 扫描 [AppStartup] → ConfigureServices(IServiceCollection) # 按 Order 升序
├─ Admin.NET.Application/Startup.cs [AppStartup(100)]
│ HostOptions、RabbitMQ、HttpRemoteOptions、MQTT 拦截器
└─ Admin.NET.Web.Core/Startup.cs [AppStartup(int.MaxValue)]
AddProjectOptions → AddCache → AddJwt<JwtHandler>(全局鉴权)
→ Signature/APIKey 鉴权 → AddCorsAccessor → AddControllersWithViews()
→ AddInjectWithUnifyResult<AdminNETResultProvider>() → AddJsonOptions
→ AddSqlSugar() ★建库建表建种子在此触发
→ OAuth/ES/限流/事件总线/OSS/视图引擎/SignalR/日志/验证码/MCP…
└─ Build()
└─ ApplicationComponent.Load(app) # 按 Order 降序执行 LoadAppComponent
└─ 扫描 [AppStartup] → Configure(IApplicationBuilder) # 中间件
└─ app.Run()5.5 中间件顺序(Admin.NET.Web.Core/Startup.cs)
app.UseResponseCompression(); // 1 gzip/br
app.UseForwardedHeaders(); // 2 Nginx 取真实 IP
// 开发/生产异常页
app.Use(移除 Furion 响应头 + Admin.NET:v2)
app.UseImageResize(...); // 3 图像裁剪缩放
app.UseStaticFiles(...); // 4
app.UseOAuth(); // 5
app.UseUnifyResultStatusCodes(); // 6 状态码统一
app.UseAppLocalization(); // 7 必须在 UseRouting 之前
app.UseAdminNetMcpFurionRouteCompat();
app.UseRouting(); // 8
app.UseCorsAccessor(); // 9 必须在 Routing 与 Authentication 之间
app.UseAuthentication(); // 10
app.UseAuthorization(); // 11
app.UseIpRateLimiting(); // 12 限流三连
app.UseClientRateLimiting();
app.UsePolicyRateLimit();
app.UseNabuMcp();
app.UseScheduleUI(...); // /schedule 看板
app.UseInject(string.Empty, ...); // Furion 动态 API + Swagger
app.UseEndpoints(endpoints =>
{
endpoints.MapScalarApiReference("sapi", ...); // Scalar UI
app.UseKnife4UI(o => o.RoutePrefix = "kapi"); // Knife4j UI
endpoints.MapHubs(); // SignalR
endpoints.MapTusUpload(); // 断点续传
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
endpoints.MapConnectionHandler<MqttConnectionHandler>("/mqtt");
});6. 配置加载
Admin.NET.Web.Entry/appsettings.json 只有一句,真正的配置都在 Admin.NET.Application/Configuration/*.json:
{
// 扫描并合并该目录下所有 json(第二个 "" 表示再扫一次根级)
"ConfigurationScanDirectories": [ "Configuration", "" ]
}常用配置文件:App.json(雪花 Id/跨域/虚拟目录)、Database.json(多库连接与 CodeFirst 开关)、JWT.json、Cache.json、Limit.json(限流)、Logging.json(日志写库)、Swagger.json、Upload.json、CodeGen.json、Captcha.json、OSSProvider、RabbitMQ、Mqtt、EventBus…
7. Swagger 分组
分组由 Configuration/Swagger.json 的 GroupOpenApiInfos 定义,业务服务用 [ApiDescriptionSettings] 归类:
"SpecificationDocumentSettings": {
"DefaultGroupName": "Default",
"EnableAllGroups": true,
"GroupOpenApiInfos": [
{ "Group": "Default", "Title": "Admin.NET 通用权限开发平台", "Order": 1000 },
{ "Group": "All Groups", "Title": "所有接口", "Order": 0 }
]
}// 归入 Default 组,Order 决定组内排序,Description 是分组说明
[ApiDescriptionSettings(Order = 490, Description = "系统用户")]
public class SysUserService : IDynamicApiController, ITransient { }
// 归入自定义组(ApplicationConst.GroupName = "main")
[ApiDescriptionSettings(ApplicationConst.GroupName, Order = 100, Description = "测试服务")]
public class TestService : IDynamicApiController, ITransient { }访问地址:Knife4j http://ip:port/kapi、Scalar http://ip:port/sapi、Swagger JSON /swagger/{Group}/swagger.json。
8. 落地一个新业务模块的步骤清单
Admin.NET.Application/Entity/Xxx.cs建实体(继承EntityBaseData,贴[SugarTable])- 如需种子数据:
Admin.NET.Application/SeedData/XxxSeedData.cs实现ISqlSugarEntitySeedData<Xxx> Admin.NET.Application/Service/Xxx/Dto/XxxInput.cs写Page/Add/Update/Delete四个 InputAdmin.NET.Application/Service/Xxx/Dto/XxxOutput.cs写 Output(敏感字段加JsonConverter)Admin.NET.Application/Service/Xxx/XxxService.cs写服务:IDynamicApiController, ITransient- 打开
Configuration/Database.json的EnableInitTable(首次),启动自动建表 - 启动后到
/kapi验证接口,再到系统菜单里挂权限按钮
⚠️ 注意事项 / 常见坑
- 改配置不要动
Web.Entry/appsettings.json,它只有ConfigurationScanDirectories;业务配置写Application/Configuration/。 [AppStartup(Order)]在ConfigureServices是升序、在ApplicationComponent的LoadAppComponent是降序,两套顺序别混用。AddSqlSugar()在ConfigureServices阶段就完成建库/建表/种子,所以种子数据里不能依赖任何尚未注册的服务。- 凡是新加的
Startup(插件或业务),必须继承AppStartup才会被扫描;只写ConfigureServices/Configure两个方法即可。 - 新增实体若忘了
[SugarTable],InitTable不会建表(它只筛IsDefined(typeof(SugarTable)))。 - 大文件上传失败先查
MaxRequestBodySize(1GB)与MultipartBodyLengthLimit(1GB),不是代码问题。
小结
记住一条链就够了:Program.cs → WebComponent(构建期三件事)→ [AppStartup] 的 ConfigureServices(AddSqlSugar 是核心,建库建表在这里)→ ApplicationComponent → 中间件按固定顺序装载 → UseInject 暴露动态 API。业务代码只落在 Application 三层目录:Entity/、Service/Xxx/Dto/、Service/Xxx/。