WHL —— 企业级 SaaS 平台底座 + 垂直业务系统,全栈自研。
不是又一套 CRUD 脚手架。这是一套能直接拿去交付企业项目的底座:后端框架、插件机制、多租户、数据库适配层、前端 Admin,五件事都自己做完了,业务模块拿来即用。
四层结构,每层职责单一,边界清晰:
┌─────────────────────────────────────────────────────────────┐
│ WHL.Web 宿主 — ASP.NET Core 10 Web API │
│ DI 注册 / 中间件管道 / config/*.json / Adapter/ / Plugins/ │
├─────────────────────────────────────────────────────────────┤
│ Sys 系统管理(框架级,编译进主程序集,不是 plugin) │
│ 用户 / 角色 / 权限 / 页面 / 数据字典 / 编码规则状态 / 在线会话 │
├─────────────────────────────────────────────────────────────┤
│ Plugins 业务插件(11 个,ALC 隔离,运行时动态加载) │
│ Platform / OAuth / Org / App / File / Codegen / │
│ Event / Scheduler / Workflow / Log / PluginManager │
├─────────────────────────────────────────────────────────────┤
│ Framework 跨插件共享底座(13 个 WHL.* 项目) │
│ Core / Core.Utility / Core.Extensions / Auth / Jwt / │
│ Cache / SqlSugar / EventBus / Share.Proxy / Plugins / │
│ DistributedLock / Swagger / Analyzers │
└─────────────────────────────────────────────────────────────┘
关键设计:Framework 与 Plugins 之间没有反向依赖。框架不知道有哪些业务插件,插件也不知道框架里除了公共抽象还有什么。新增业务模块 = 建一个 plugin 目录 + 一个 manifest.json,主程序一行不改。
| 项目 | 职责 |
|---|---|
WHL.Core |
基础抽象:ICurrentUser / ICurrentTenant / ICurrentCompany / IApplicationService / SysException / SaaSOption / BaseService
|
WHL.Core.Utility |
Helper + MVC Filter + 中间件:TenantResolutionMiddleware / HttpRequestMiddleware / StringTrimFilter / DataValidationFilter / DbConnectionFactory / AliyunSmsSender
|
WHL.Core.Extensions |
DI 与映射:AppApplicationBuilderExtensions / Autofac 容器 / AutoMappingAttribute 自动映射 |
WHL.Auth |
认证契约:IAuthToken / ITokenResult / CurrentUserExtensions
|
WHL.Jwt |
JWT 签发与鉴权:JwtAuthToken / JwtPermissionAttribute / AuditMiddleware / CurrentUser / CurrentTenant / JwtTokenSessionStore
|
WHL.Cache |
缓存抽象与实现:ICacheService + 工厂模式(Process / Sql / Redis / NCache)+ 诊断接口(枚举 entries / 统计 / TTL) |
WHL.SqlSugar |
ORM 底座:SqlSugarScope / SqlSugarDbContext / UnitOfWork / Repository / BaseTable / CustomDbType 21 种 DB / 租户分库路由 |
WHL.EventBus |
事件总线:Contracts 契约 + InMemory / 外部总线后端 + EventPublisher / EventSubscriber + AOP 拦截 |
WHL.Share.Proxy |
跨插件调用契约:IProxy*Service 接口 + 共享 DTO 模型 |
WHL.Plugins |
插件动态加载:PluginLoadContext(可回收 ALC)/ PluginManager / PluginLoadPlan / 健康检查 |
WHL.DistributedLock |
分布式锁:IDistributedLockService,支持 SqlServer / MySQL / Redis / NCache 后端,锁 key 带租户命名空间 |
WHL.Swagger |
Swashbuckle 10 集成 + SwaggerSummaryOperationFilter 摘要过滤 |
WHL.Analyzers |
Roslyn 源码分析器(HttpVerbParameterAnalyzer 等),把规范约束到编译期 |
目录结构
WHL.Web/Plugins/{PluginCode}/
├── manifest.json code / name / version / order / enabled / loadOrder
├── WHL.<Code>.Domain.dll 实体
├── WHL.<Code>.IService.dll 接口 + DTO
├── WHL.<Code>.Service.dll 实现
└── WHL.<Code>.Web.dll Controller
manifest.json 样例
{
"code": "Org",
"name": "组织架构",
"version": "0.1.0",
"order": 200,
"enabled": true,
"loadOrder": [
"WHL.Org.Domain.dll",
"WHL.Org.IService.dll",
"WHL.Org.Service.dll",
"WHL.Org.Web.dll"
]
}
加载流程
AddDynamicPlugins()
→ 扫 Plugins/*/manifest.json
→ 每个 plugin 一个 AssemblyLoadContext(isCollectible: true,可热卸载)
→ 解析顺序:默认 ALC 已加载 → 其他 plugin ALC 已加载(跨 plugin 共享,MD5 校验)→ plugin 目录 → 主目录
→ 找不到 → fail-fast 抛 FileNotFoundException(启动期暴露,不静默)
→ AddApplicationPart 注入 MVC 路由 + ConfigureServices 注册 DI
→ 预加载阶段保留 ALC 引用,防 GC 提前回收
跨插件调用 — 插件之间不直接引用 DLL,统一走 IProxy*Service:
Plugin A ──HttpClient──→ Proxy 拦截器(自动透传 X-Tenant)──→ Plugin B
ApiInterceptorService 在拦截器里自动把当前请求的 X-Tenant 头带过去,租户上下文跨插件调用不丢。
热插拔 — IPluginManager.LoadAsync 支持运行时加载 / 卸载插件,isCollectible: true 让 DLL 文件在卸载后可以被替换或删除。
租户 / 行政区域 / 通知配置(邮件 + 短信)/ 编码规则 / 数据库类型管理 / 缓存诊断 / 分布式锁管理 / 运行统计。
内置管理诊断面板:缓存条目枚举 / 缓存统计 / 分布式锁状态 / 编码规则生成,是线上排障的入口。
认证客户端 / 认证提供者 / 提供者租户 / 三方用户 / 授权码 / 访问令牌 / 刷新令牌。
多登录方式收敛到一处:OAuth2、TokenGuid、JWT、Soap 协议都走同一套客户端与 Provider 模型,前端只对接一个登录入口。
组织 / 岗位 / 部门 / 员工 / 员工 N:M 部门与岗位联动 / 组织图 / 变更日志 / 数据字典。
应用注册 / 应用插件更新 / 应用运行 / 数据库管理 / 数据字典 / 脚本执行 / 服务器与部署 / 租户绑定与审计。
面向多应用交付:一个平台承载多个业务应用,应用级数据库绑定、服务器部署、租户授权都在这里闭环。
物理文件存储 / 分类 / 内容类型 / 磁盘用量统计 / 公开访问路径签发。
库表结构 / 外部库接入 / 数据字典 / SQL 执行 / 模板管理 / 一键生成实体与页面。
从库里一张表到「实体 + IService + Service + Controller + 页面 + 菜单」一条龙,配合后端路由自动同步菜单,新模块落地不用手抄 CRUD。
事件主题 / 事件规则 / 订阅 / 手动投递 / 失败重试 / 消息队列 / 通知。
规则引擎 + 失败重试 + 消息队列三件套,业务侧发事件、订阅端按规则消费,失败自动重投,不再手写回调。
定时任务 / 任务包 / 运行记录 / Worker 池 / 实例画像 / 表单 Agent / 死信队列 / 统计。
多调度模式 + Worker 实例画像 + DLQ 死信队列,任务失败可追可重投。
流程定义 / 版本 / 节点审批配置(组织 / 机器人 / 外部 / 动态)/ 子流程 / 实例 / 队列 / 我的任务 / 通知。
版本化流程定义 + 节点级审批策略,流程变更不影响在跑的实例。
HTTP 访问日志 / SQL 审计日志 / 登录日志 / 系统日志,按天分表落库(log_http_{yyyyMMdd} / log_sql_{yyyyMMdd}),文件 + 数据库双写,异步 channel 攒批,业务线程零阻塞。
插件列表 / 版本管理 / 挂载 / 数据库迁移。配合 WHL.Plugins 的 ALC 机制,支撑插件的安装、启停、升级与热加载。
WHL 不写 adapter 代码,只写接口 + 反射加载机制。
| 类别 | 数据库 |
|---|---|
| 商业 / 主流 | MySQL、SQL Server、Oracle、PostgreSQL、SQLite、Access |
| 国产化 | 达梦 DM、金仓 KingbaseES、神通 Oscar、openGauss、GaussDB、瀚高 HighGo、GBase、OceanBase、PolarDB、TDSQL、MariaDB |
| 分析型 / 冷门 | IBM Db2、ClickHouse、DuckDB |
WHL.Web/Adapter/
├── SqlServer/ MySql/ Oracle/ PostgreSql/ Sqlite/
├── DM/ Kingbase/ Oscar/ openGauss/ GaussDB/
├── OceanBase/ PolarDB/ TDSQL/ GBase/ HighGo/
├── MariaDB/ ClickHouse/ DB2/ DuckDB/
扩展机制(零代码):
// 1) Framework/WHL.SqlSugar/Custom/CustomDbType.cs 加一个字段
[CustomDbTypeMeta("MySQL", DbType.MySql, "MySQL — 最流行的开源关系型数据库")]
MySql = 0,
// 2) WHL.Web/Adapter/{枚举名}/ 放 driver dll
枚举名与 Adapter 物理目录名 1:1 对应,DbConnectionFactory 反射装载。前端下拉、连接测试、预热驱动全部自动出现,不用改前后端任何代码。
TenantResolutionMiddleware 在 UseRouting 之前运行,三种 URL 模式都路由到同一 controller:
// config/saas.json — 字段默认值取自 Framework/WHL.Core/Configuration/SaaSOption.cs
"SaaS": {
"Mode": "Both", // None | Path | Both
"PathPrefix": "/t/", // URL 改写前缀
"DefaultTenant": "master", // 无 prefix 时的 fallback(源码默认空串,按部署配)
"PlatformPathPrefix": "/api/platform/", // bypass 路径(源码默认)
"RewritePath": true // 是否改写 Request.Path 去掉 /t/{slug}
}
| 模式 | 请求 | 落到 |
|---|---|---|
None |
/platform/... |
master 租户(单租户兼容) |
Path |
/t/{slug}/platform/... |
显式租户(URL 改写后匹配) |
Both |
两种都支持 | 对应租户 |
Both 模式下的完整链路:
请求 /t/master/platform/stats/overview
↓ TryRewritePath:Request.Path → /platform/stats/overview,slug="master" 写 Items
↓ IsPlatformPath? 否
↓ Mode=Both → ResolveSlugAsync("master") → 拿 TenantIdentity
↓ context.Items[ItemsKey] = "master"
↓ Request.Headers["X-Tenant"] = tenantNo
UseRouting:匹配 /platform/stats/overview → StatsController
↓
JwtPermissionAttribute:读改写后的 Request.Path → 跟 DB RouteUrl 精确匹配
租户隔离贯穿:缓存 namespace 前缀、租户级配置表(每租户 1 条 + 系统租户 fallback)、SqlSugar 全局过滤、跨插件 Proxy 头透传。
[JwtPermission(PageCode = 20001, IsAuthCheck = true)]
public async Task<ApiResult> Save(XxxModel entity) { ... }
AreaGroupAttribute + JwtPermissionAttribute 生成 sys_page,前端不用手写菜单表PageCode 全解决方案唯一,冲突在启动期直接抛异常IsExactMatch(DB RouteUrl, 改写后 Request.Path),DB 存无 prefix URL,1:1 匹配IsAuthCheck = false 表示完全公开端点| 能力 | 说明 |
|---|---|
| 事件日志 | 4 类事件(System / Sql / Http / Login)× 5 级日志级别(Ignore / Default / OverTime / Error / Exception),按天分表落库,文件 + DB 双写 |
| 异步写入 |
LogChannelWriter channel 攒批(默认队列 10000 / 批量 100),SQL 日志在业务线程零阻塞 |
| 缓存诊断 | 后端名 / entries 枚举 / 命中统计 / TTL 倒计时 / 单条 value 查看,内置管理面板 |
| 分布式锁 | 可视化查看锁状态,锁 key 带租户命名空间 |
| 死信队列 | Scheduler + Event 失败任务进 DLQ,可追可重投 |
| 审计 |
AuditMiddleware + 4 件套审计列(CreateDate / CreateByUserId / LastModifyDate / LastModifyUserId)+ IsDeleted 软删 |
CustomDbType)走 attribute 反射,不写表
IHostedService 反射读取插件 Enum 命名空间,自动 upsert 父 + 子 row,前端按 parentNo 取子项platform_coding_rule):规则编码 / 名称 / 前缀 / 后缀 / 流水步长 / 起始值 / 日期前缀类型,按应用维度配置,支持按应用编码(如 C_)生成业务单号| 仓库 | 定位 |
|---|---|
| whl.admin.backend | 后端框架 + 宿主(Framework 13 项目 / Sys 系统管理 / WHL.Web 宿主 / DB Adapter) |
| whl.admin.plugins | 11 个业务插件(Platform / OAuth / Org / App / File / Codegen / Event / Scheduler / Workflow / Log / PluginManager) |
| whl.admin.front | 前端(Vue 3 + Element Plus + vue-pure-admin + Vite + TypeScript) |
环境要求
CustomDbType 覆盖的任意一种适配数据库4000
初始化数据库
CodeFirst 已禁用,所有表手写 DDL。框架级 + 核心模块一条龙:
mysql -u root -p < backend/docs/init/init-all.sql
init-all.sql 按依赖顺序执行:sys → pluginmanager → file → platform → plugin/codegen。其余插件的 schema / seed 在各自仓库目录(见 backend/README.md 表格)。
启动
dotnet build backend/WHL.sln
dotnet run --project backend/WHL.Web
Swagger UI:http://localhost:4000/api/doc
Windows 服务部署
WHL.Web.exe 可注册为 Windows 服务 whl.service(StartType=Auto,进程退出自动拉起),部署目录 D:\wwwroot\whl.service\:
sc create whl.service binPath= "D:\wwwroot\whl.service\WHL.Web.exe" start= auto
sc start whl.service
return Success(await _service.Xxx(args));
/// <summary> 注释Async 后缀:Task<X> GetById
Find 开头:FindById / FindAll
enabled 参数统一 bool?(null=全部 / true=启用 / false=禁用)IsDeleted 永远最末尾、索引 idx_<table>_<col>、DPAPI 加密列宽度 1000[controller] = 类名去 Controller 后缀X-Tenant,否则租户隔离失效WHL 目前是单人维护组织,接受以下形式的参与:
项目采用 MulanPSL-2.0 开源协议,商业使用前请阅读协议条款。
如果你觉得 WHL 对你的项目有帮助,欢迎支持一杯咖啡 ☕
License: MulanPSL-2.0