外观
VOL Pro 后端源码分析
对象:
api_sqlsugar/(.NET 8 + SqlSugar/EF 双 ORM),版本 VOL Pro 3.5.8。 定位:源码级架构解析(怎么实现的/为什么这样设计)。写业务代码请查《开发指南-后端.md》,两份互补。 所有结论均基于源码实证,标注文件:行号。
1. 解决方案全景
| 项目 | .cs 规模 | 职责 | 改动边界 |
|---|---|---|---|
| VolPro.WebApi | 启动项目 | Program.cs、全部 Controllers(按模块分文件夹) | 自定义接口写 Controllers/**/Partial/ |
| VolPro.Core | ~40 目录 | 框架内核:BaseProvider/WorkFlow/FormWorkFlow/Quartz/SignalR/Tenancy/Filters/CacheManager | 尽量不改 |
| VolPro.Entity | — | 全部实体 DomainModels/(Core/System/erp/mes/ai/flow...) | 主文件生成器产物不改,扩展写 partial |
| VolPro.Sys | — | 系统模块:用户/菜单/角色/字典/日志/工作流配置 | 业务扩展写 Services/**/Partial/ |
| VolPro.ERP | 42 单据 | 官方 ERP 示例(最完整的业务范式) | ours,随便改 |
| VolPro.Mes | 189 | MES 骨架示例(Partial 多为空壳) | ours |
| VolPro.AI | 101 | AI 助手/知识库/数据分析平台 | ours |
| VolPro.Builder | — | 代码生成器(Sys_TableInfo → 四层产物 + Vue 页面) | 不改 |
| VolPro.DbTest | 63 | 多数据库兼容性测试样例 | 可删 |
引用方向:WebApi → (Sys/ERP/Mes/AI/Builder/DbTest) → Core → Entity。
分层铁律:控制器在 WebApi、Service/Repository 在业务模块项目、实体在 Entity——一个"表"的四层文件横跨三个项目,靠命名空间 + 生成器对齐。
2. 启动流程(Program.cs 顺序)
文件:VolPro.WebApi/Program.cs
1. AppSetting.Init 加载 appsettings.json 到静态配置类
2. 建 WebApplicationBuilder
3. Kestrel 监听 9100
4. DI 注册(顺序敏感):
- 控制器 + NullObjectModelValidator(关掉自动400, 框架自己校验参数)
- Swagger(单分组 v1, RoutePrefix="" 挂根路径)
- CORS 策略 "cors": SetIsOriginAllowed(_=>true)+AllowAnyHeader+AllowCredentials ⚠️全放行
- JwtBearer(签发参数来自 appsettings: Secret + ExpMinutes)
- IHttpContextAccessor / IHttpClientFactory
- Redis(UseRedis=true 时换 RedisCacheService, 否则 MemoryCacheService)
- SignalR(UseSignalR=true 时)
- 业务 Service/Repository: 按程序集批量扫描注册(Scoped)
5. 数据库初始化:
- EF: EFDbContext 按连接串节点名建类(SysDbContext/ServiceDbContext/TestDbContext)
- SqlSugar: DbManger.ServiceDb 动态建 client(支持运行时分库 ConfigId)
6. Upload 目录不存在则创建
7. 中间件管道(顺序即执行顺序):
ExceptionHandlerMiddleWare → CORS → UseStaticFiles(/Upload 物理映射)
→ UseRouting → UseAuthentication → UseAuthorization → MapControllers/Hub → Swagger要点:
- 异常中间件在最外层,同时承担:停用账号检查、请求计时、请求日志落库(
ExceptionHandlerMiddleWare.cs:28-100)。 - 文件授权:
FileAuth=1时/Upload?access_token=走 JWT 校验(ExceptionHandlerMiddleWare.cs:47-82),默认 0 直接放行。
3. 一个请求的完整生命周期
以 POST api/ERP_PurchaseOrder/getPageData 为例(全部源码实证):
Kestrel(9100)
→ ExceptionHandlerMiddleWare.Invoke 停用检查/计时/FileAuth
→ CORS("cors") → UseRouting → 端点匹配
→ JwtBearer 中间件 验签名/Issuer/Audience/Exp
→ MVC 过滤器链:
① ApiAuthorizeFilter.OnAuthorization (IAuthorizationFilter, 全局)
匿名+带token→补Identity; 临期→响应头 vol_exp:1
② [FromBody] 模型绑定 (NullObjectModelValidator 不产生自动400)
③ ActionPermissionFilter.OnActionExecutionAsync
[ApiActionPermission(Search)] → Sys_Menu权限 UserAuthArr 判定
④ ActionExecuteFilter.OnActionExecuting ActionParamsValidator() 参数校验
→ ERP_PurchaseOrderController.GetPageData [ApiBaseController.cs:32-38]
→ InvokeService 反射转发到 Service 层 [ApiBaseController.cs:358-361]
→ ServiceBase<ERP_PurchaseOrder>.GetPageData [ServiceBase.cs:49-75]
→ 返回 PageGridData → JsonNormal 序列化 → 请求日志落库3.1 查询管道(Service 层)
ServiceBase.cs:49-75 → BuildPageDataQuery(ApplicationServiceBaseSearchExtensions.cs:26-50)内部顺序:
扩展字段注入 → 参数校验/条件解析(Filter/Whes)→ 租户过滤 FilterTenancy → QueryRelativeExpression 业务钩子 → 逻辑删除过滤 FilterLogicDel → Count → 排序 → TakePage → 字段级权限 FilterQueryableAuthFields → 合计行 InvokeSummaryExpress。
3.2 Repository → 双 ORM
RepositoryBase.cs:46-57:DbContext(EF)与SqlSugarClient属性指向同一ServiceDbContext;BaseDbContext.Set<T>()→SqlSugarClient.SetDbSql<T>()返回ISugarQueryable<T>——EF 的 DbContext 在这里只是壳,查询真正跑 SqlSugar。- 事务统一走
DbContextBeginTransaction(RepositoryBase.cs:68-102,Ado.BeginTran/CommitTran/RollbackTran)。 - 库路由:
ServiceDbContext→DbManger.ServiceDb→ 动态分库时按UserContext.CurrentServiceId挂新 ConfigId。
3.3 返回信封与双序列化出口
- 信封
WebResponseContent:{ status, code, message, data },Code是ResponseType枚举数字(WebResponseContent.cs:16-20,91)。 - 两个序列化出口(
VolController.cs:21-31):JsonNormal():保留 C# 原字段大小写 + 日期yyyy-MM-dd HH:mm:ss+ long→string——查询类接口用它(前端表格依赖 PascalCase);Json():全局驼峰——增删改接口用它。
- 异常统一 500 +
{message, status:false};认证失败 401 +{message:"授权未通过"}。
4. 内核:BaseProvider 体系(26 文件)
4.1 类层次
ApiBaseController<TService> VolPro.Core/Controllers/Basic/
└ InvokeService 反射调用 Service 同名方法
IService<T>, ServiceBase<T> 生成器产出接口+骨架, Partial 重写
└ ApplicationServiceBase 委托钩子定义区(57-150行)
IRepository<T>, RepositoryBase<T> 数据访问
└ ServiceDbContext / DbManger SqlSugar client 工厂4.2 委托钩子全清单(ApplicationServiceBase.cs:57-150)
业务扩展不靠 override 靠赋值委托(Partial Service 里写 AddOnExecuting = (...) => {...} 即生效):
| 类别 | 钩子 | 时机 |
|---|---|---|
| 查询 | QueryRelativeList / QueryRelativeExpression | 查询构建期,注入自定义条件 |
| 查询 | OrderByExpression | 默认排序覆盖 |
| 查询 | SummaryExpress | 合计行(配合 SqlFunc.AggregateSum) |
| 查询 | GetPageDataOnExecuted | 查询返回后加工 |
| 写入 | AddOnExecuting / AddOnExecuted | 事务内校验/事务后动作 |
| 写入 | UpdateOnExecuting / UpdateOnExecuted | 同上 |
| 删除 | DelOnExecuting / DelOnExecuted | 同上 |
| 审核 | AuditOnExecuting / AuditOnExecuted | 审核前后 |
| 工作流 | AuditWorkFlowExecuting / AuditWorkFlowExecuted | 流程审核前后 |
纪律:override 虚方法时末尾必须 return base.Xxx(...);钩子里 return WebResponseContent.Error() 即整体回滚(base.Add 内部已开事务)。
4.3 保存管线(SaveModel 约定)
主表+明细经 SaveModel.MainData / SaveModel.DetailDatas(按 tables 明细集合名分组);删除明细靠 delKeys 约定(不在本次提交中的已有行按主键删除);审计字段 CreateID/Creator/CreateDate/ModifyID/... 由 SetCreateDefaultVal()/SetModifyDefaultVal() 在管线内自动填(受 appsettings: CreateMember/ModifyMember 开关控制,见 Sys_MenuService.cs:229/245 用法)。
5. ApiBaseController 通用端点清单
所有生成控制器自动获得(路由模板 api/{表名}/{Action},camelCase):
| Action | 权限 | 说明 |
|---|---|---|
getPageData / getPageDataAsync | Search | 分页查询(唯一默认出现在 Swagger 的) |
GetDetailPage / getDetailPageAsync | Search | 明细表分页 |
Add / Update | Add / Update | 新增/修改(SaveModel) |
Del / delAsync | Delete | 按主键数组删除 |
Audit / auditAsync | Audit | 审核(auditStatus/auditReason) |
antiAudit / antiAuditAsync | Audit | 反审核 |
cancelAudit / urgentAudit | Audit | 撤审/加签 |
Export / exportAsync | Search | Excel 导出 |
Import / importAsync | Import | Excel 导入 |
DownLoadTemplate | Import | 下载导入模板 |
Upload / uploadAsync | Upload+Add+Update | 文件上传 |
Partial 控制器可加第二 [Route](如 Sys_UserController 主 api/Sys_User + Partial api/User 承载 login/replaceToken),用 [ActivatorUtilitiesConstructor] 抢占构造注入。
6. 认证与授权链路
6.1 JWT
- 签发:
JwtHelper.cs:18-46,claims 全字符串(jti=UserId/iat/nbf/exp/iss/aud),HS256,key=AppSetting.Secret.JWT。 - 校验:JwtBearer 中间件(ValidIssuer/ValidAudience 均取 appsettings)。
- 无独立 RefreshToken 表,采用"剩余寿命检测 + 无感置换":
ApiAuthorizeFilter.cs:73-79:临期(剩余 < ExpMinutes/3)响应带vol_exp: 1头;HttpRequestMiddleware.cs:16-21:Access-Control-Expose-Headers: vol_exp让跨域前端可见;POST /api/User/replaceToken(Partial/Sys_UserController.cs:91-129):缓存键rp:Token:{userId}+ 进程锁防并发置换。 (注意:Sys_UserService.ReplaceToken()是空实现,真逻辑在 Controller。)
6.2 用户上下文
UserContext.Current(Scoped):
UserId:Claims 取 Jti(UserContext.cs:509-516);UserInfo:缓存优先(Redis/Memory 按 userId 键),未命中查 Sys_User 回写;一次请求只读一次缓存;IsSuperAdmin:Role_Id=1 直通所有权限判定。
6.3 权限三层
- 认证层:JWT 签名/过期(中间件);
- 接口层:
ApiAuthorizeFilter(全局)+[ApiActionPermission(ActionPermissionOptions.XXX)](Action 级)→ 查Sys_Menu.Auth中该表的按钮权限值;[PermissionTable(Name=...)]绑定表名; - 数据层:字段级权限
FilterQueryableAuthFields()+ 只看自己(UserAuth=1时按 CreateID 过滤)+ 租户过滤(§8.4)。
7. 进程缓存三件套(菜单/字典/权限)
统一模式:静态字段 + 双检锁 + 共享缓存版本号(Partial/Sys_MenuService.cs:35-117 为代表):
csharp
private static List<Sys_Menu> _menus; // 进程内缓存
private static object _menuObj = new(); // 双检锁
private static string _menuVersionn = ""; // 本进程版本号
private const string _menuCacheKey = "inernalMenu";
GetAllMenu():
CacheContext.Get(_menuCacheKey) != _menuVersionn → 加载库 → 回写 _menus
Save/Del:
CacheContext.Add(_menuCacheKey, DateTime.Now.Ticks...) // 版本号一变, 所有实例下次读取即失效CacheContext是ICacheService的门面:UseRedis=true→RedisCacheService(多实例联动失效靠它),否则MemoryCacheService(单实例)。- 直接改库(SQL 手改菜单/授权)不失效——版本号只在走 Service 的 Save/Del 时刷新;手改库后须后台保存一次菜单或重启。
- 权限/字典缓存同款机制(
UserContext.GetPermissions/Sys_DictionaryService)。
8. 内核横切组件
8.1 两套工作流的区别(易混,必背)
| WorkFlow(21 文件) | FormWorkFlow(10 文件) | |
|---|---|---|
| 定位 | 业务表审批流(ERP 单据审核) | 表单设计器流程(自定义表单) |
| 注册 | 按表注册 WorkFlowTableOptions | 流程模板 Sys_FormWorkFlowStep |
| 入口 | WorkFlowManager.AddProcese<T>()(新建单据时写入流程实例) | FormWorkFlowManager(ApplyStep 实例化) |
| 能力 | 顺序步骤/反审核(WorkFlowAntiAuditExtensions) | 条件/并行分支(ParentId)/会签(Weight)/加签(Sign)/通知(Notify) |
| 实体 | Sys_WorkFlow*(flow/ 目录) | Sys_FormWorkFlow*(formFlow/ 目录) |
ERP 单据审核走 WorkFlow(Audit 钩子 + AuditStatus 字段约定);要图形化拖流程设计器才用 FormWorkFlow。
8.2 Quartz 定时任务(7 文件)
- HTTP 回调型为主:
HttpResultfulJob.cs:24-70——IHttpClientFactory 按配置发 GET/POST(AuthKey/AuthValue 请求头 + PostData + TimeOut),结果写 Sys_QuartzLog;[DisallowConcurrentExecution]防重叠。 IOCJobFactory:让 Job 走 DI 容器;QuartzNETExtension.AddServiceQuartzNet:启动时从 Sys_QuartzOptions 表恢复调度。QuartzAuthorization:远程管理回调用appsettings: QuartzAccessKey鉴权。
8.3 SignalR(8 文件,UseSignalR=true 启用)
MessageHub:仅连接生命周期管理(MessageHub.cs:27-41),连接池交IMessageService(CacheManager 存储);MessageService/MessageNotification:服务端推送 API(按用户/角色/全体);BackgroundMessageService:后台队列消费推送。- ⚠️ appsettings
CorsUrls注释提示 SignalR 场景需要,但当前 CORS 策略未引用该节点(§12)。
8.4 Tenancy 多租户(5 文件)
- 同库租户(
TenancyField配置):TenancyManager.GetSearchQueryable(TenancyManager.cs:26-33)——超管放行;默认按表有无CreateMember.UserIdField(CreateID)字段自动过滤 = 创建人维度隔离;switch (tableName)可为特定表定制(源码里有完整的角色/子角色/部门过滤示例,TenancyManager.cs:44-60)。 - 动态分库(
UseDynamicShareDB=1):每客户独立库,DbManger按CurrentServiceId挂 ConfigId,物理路径DBPath/DBBackPath。
8.5 Filters 清单(10 文件)
| 过滤器 | 类型 | 职责 |
|---|---|---|
| ApiAuthorizeFilter | IAuthorizationFilter(全局) | JWT 身份补全 + vol_exp 头 + 匿名白名单 |
| ActionPermissionFilter | IAsyncActionFilter | [ApiActionPermission] 按钮权限判定 |
| ActionExecuteFilter | IActionFilter(全局) | ActionParamsValidator 参数校验 |
| ServiceFunFilter | — | Service 委托钩子的载体(非 MVC 过滤器) |
| ActionLogAttribute | 特性 | 操作日志(Sys_Log) |
| GlobalFilter | 配置 | 演示环境 Action 黑名单(非 admin 只读,Enable=false) |
8.6 其他内核目录速览
CacheManager(4) 缓存双实现 / Dapper(5) 裸 SQL 场景 / EFDbContext(7) EF 壳 / DbSqlSugar(4) Sugar 工厂 / ObjectActionValidator(7) 参数校验器 / PerformanceMonitor(9) 性能监控后台服务(montior=1 启用) / Print(7) 打印 / WeChat(7) 微信 / KafkaManager(6) Kafka / BackgroundServices(6) 后台队列(AI 聊天落库同款 ConcurrentQueue 模式)/ Generic(15) 通用表服务(不写代码直接暴露任意表的 CRUD,由 Sys_TableInfo 驱动)/ Language(2) 多语言包。
9. 代码生成器(VolPro.Builder)
- 输入:Sys_TableInfo / Sys_TableColumn(表/列/查询/明细配置,界面维护)。
- 模板:HTML 占位符替换(非 Razor);读取表结构走各库 Provider(
Builder/Services/DataBase/MySqlTableProvider.cs等,information_schema)。 - 输出(
Sys_TableInfoService.cs):- 实体(EF + SqlSugar 双特性双写:
[Table]/[Key]+[SugarTable]/[SugarColumn(IsPrimaryKey,IsIdentity)],导航两边合用[ForeignKey]+[Navigate]); - Service 接口+骨架 / Repository / Controller(Partial 永不覆盖);
- 前端:
views/{folder}/{Table}.vue+extension/{folder}/{Table}.jsx(vite 版,2024.03.16 起);App 端三件套并自动改 pages.json 注册路由; - 菜单直接写 Sys_Menu 表(
BuilderController.cs:140-235,非生成 SQL)——默认只给 Search 权限,普通角色需手动授权。
- 实体(EF + SqlSugar 双特性双写:
syncTable:库表结构变更后同步列配置;gengeneric=true:只配置不生成代码(走 Generic 通用服务)。- 生成器接口全部
[ApiActionPermission(ActionRolePermission.SuperAdmin)]。 - 无 CoderV3 双版本:本仓库单套生成器,演进点全部以日期注释内联标注。
10. Entity 组织要点
DomainModels/按业务域分子目录(Core/System/erp/mes/ai/flow/formFlow/Dashboard/Quartz...)。- 无 BaseEntity 审计基类——审计六字段由生成器逐字段写进每个实体,赋值靠 Save 管线。
- 主实体文件是生成器产物不可改;业务扩展字段统一写
DomainModels/erp/ErpBillExt.cs这类 partial 文件(如ReceivedQty下游反写字段、MaterialId库存过账字段、ErpBillStatus单据状态枚举)。 - 手写扩展字段不必重复双 ORM 特性——只写
[Column(TypeName)]等数据注解即可被双 ORM 识别(导航属性除外)。
11. 业务模块范式(官方怎么写)
11.1 ERP(42 单据,业务范式标杆)
四层 + Partial:Controller 薄壳(ApiBaseController 反射转发)→ Service 骨架(空壳)→ Services/erp/Partial/XxxService.cs(全部业务:Add/Update 委托三件套校验、明细合计、库存过账在 AuditOnExecuting)→ ErpBillExt.cs(扩展字段)。
主从保存约定:SaveModel.DetailDatas(明细集合按 tables 名分组)+ delKeys 差集删除。
11.2 值得抄的实现
| 位置 | 亮点 |
|---|---|
VolPro.Mes/Services/mes/Partial/MES_ProductionOrderService.cs:42-65 | 官方"列表合计行"标准写法:SummaryExpress(主表 SqlFunc.AggregateSum) + override GetDetailSummary<Detail>(明细合计) |
VolPro.AI/Services/Chat/ChatPersist/ChatPersistBackgroundService.cs | 后台队列落库(ConcurrentQueue 模式,Logger 同款) |
VolPro.AI/Services/ai/DataAnalysisSqlGuard.cs | AI 生成 SQL 的安全防护(8.7KB 白名单/黑名单/DDL 拦截) |
VolPro.AI/Services/ai/Partial/AI_TableService.cs:47-60 | 把 Sys_TableInfo 元数据喂给 AI 做表上下文(并屏蔽 sys_user 敏感表) |
VolPro.AI/KnowledgeChunkMemoryCache + Retriever | 知识库分块检索缓存 |
11.3 DbTest
多数据库兼容验证样例(SqlServer/MySql/PgSql/达梦/Oracle),Demo_OrderService.cs(5.9KB) 是主从+查询扩展示例,非生产范式来源。
12. 配置全景(appsettings.json)
| 节点 | 作用 | 提示 |
|---|---|---|
Connection.DBType / DbConnectionString / ServiceDbContext / TestDbContext | 库类型 + 三库连接 | ⚠️ 明文连接串 |
RedisConnectionString / UseRedis | 缓存实现切换 | ⚠️ 密码 123456 |
UseSignalR | 消息推送开关 | |
Secret | JWT 签发 + DES 密钥组 | ⚠️ 硬编码入仓库 |
GmCrypto | 国密 SM2/3/4 | KeyHex 空=未启用 |
ExpMinutes | JWT 有效期(分钟),默认 120 | |
CreateMember / ModifyMember | 审计字段自动填充开关 | |
GlobalFilter | 演示环境只读黑名单 | Enable=false |
UseSnow | bigint 雪花 ID | 0 |
QuartzAccessKey | 定时任务远程回调密钥 | ⚠️ 硬编码 |
LogicDelField | 逻辑删除字段(空=物理删除) | |
TenancyField / UseDynamicShareDB / DBPath / DBBackPath | 租户隔离/动态分库 | |
UserAuth | 只看自己创建的数据 | 0 |
FileAuth | /Upload 静态文件授权 | 0 |
RateLimiting | 按 IP+路径限流 | Enabled=false |
GenControllerToDir / montior / DbTable | 生成器/监控/表维护开关 | |
CorsUrls | 跨域白名单 | ⚠️ 当前 CORS 策略未引用,实际 SetIsOriginAllowed(_=>true) 全放行 |
13. 安全与部署注意(上线前必查)
- CORS 全放行(
Program.cs:130-139):任意来源+凭证;生产应收紧为白名单。 - Swagger 常开(挂根路径,
/302 → /swagger/);生成方法虽被ApiExplorerSettings隐藏,但自定义接口全量暴露,需手动关闭。 - 明文连接串/JWT 密钥/Quartz 密钥入仓库(§12 ⚠️ 项),应迁移环境变量或密钥管理。
- 静态上传目录默认无授权(FileAuth=0),Upload 可匿名枚举访问。
- 菜单/权限缓存不感知直接改库(§7),运维 SQL 改菜单后必须刷版本或重启。
- 限流默认关闭(RateLimiting.Enabled=false)。
14. 架构评价与扩展点汇总
一句话评价
经典的"代码生成 + 约定优于配置"分层 CRUD 框架:业务切入点高度收敛(Service 委托钩子),生成与手写靠 Partial 物理隔离,代价是强约定(路由/字段/缓存模式都定死)+ 查询灵活性受限(复杂报表仍可 Dapper/原生 SQL 逃逸口)。
想扩展框架行为时的注入位置(速查)
| 想做什么 | 去哪里 |
|---|---|
| 加自定义接口 | Controllers/**/Partial/,自动带 JWT |
| 改业务校验/联动 | Services/**/Partial/ 委托钩子(§4.2) |
| 改查询条件/排序/合计 | QueryRelativeExpression / OrderByExpression / SummaryExpress |
| 单据审批 | WorkFlow:注册 WorkFlowTableOptions + Audit 钩子 |
| 定时任务 | Sys_QuartzOptions 配 HTTP 回调(AuthKey 头),或 IOCJobFactory 写程序集 Job |
| 推送消息 | MessageNotification(SignalR 开关) |
| 数据隔离 | TenancyManager switch 定制 / UserAuth / 字段级权限 |
| 换缓存实现 | ICacheService(Redis/Memory 已备,可扩展) |
| 全局参数校验 | ObjectActionValidator / ActionExecuteFilter |
| 不写代码暴露表 CRUD | Generic 通用服务(Sys_TableInfo 配置驱动) |
15. 逻辑关系示意图(Mermaid)
15.1 总体分层架构
15.2 请求生命周期(POST api/表名/getPageData)
15.3 保存流程与委托钩子事务边界(Add 为例)
15.4 认证与无感置换 Token
15.5 权限三层判定
15.6 进程缓存三件套:版本号联动失效(菜单为例)
15.7 双 ORM 真相
15.8 代码生成器契约
2026-11-02 基于源码全量探查整理;示意图章节同日补绘。