Skip to content

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.ERP42 单据官方 ERP 示例(最完整的业务范式)ours,随便改
VolPro.Mes189MES 骨架示例(Partial 多为空壳)ours
VolPro.AI101AI 助手/知识库/数据分析平台ours
VolPro.Builder—代码生成器(Sys_TableInfo → 四层产物 + Vue 页面)不改
VolPro.DbTest63多数据库兼容性测试样例可删

引用方向: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 / getPageDataAsyncSearch分页查询(唯一默认出现在 Swagger 的)
GetDetailPage / getDetailPageAsyncSearch明细表分页
Add / UpdateAdd / Update新增/修改(SaveModel)
Del / delAsyncDelete按主键数组删除
Audit / auditAsyncAudit审核(auditStatus/auditReason)
antiAudit / antiAuditAsyncAudit反审核
cancelAudit / urgentAuditAudit撤审/加签
Export / exportAsyncSearchExcel 导出
Import / importAsyncImportExcel 导入
DownLoadTemplateImport下载导入模板
Upload / uploadAsyncUpload+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 表,采用"剩余寿命检测 + 无感置换":
    1. ApiAuthorizeFilter.cs:73-79:临期(剩余 < ExpMinutes/3)响应带 vol_exp: 1 头;
    2. HttpRequestMiddleware.cs:16-21:Access-Control-Expose-Headers: vol_exp 让跨域前端可见;
    3. 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 权限三层 ​

  1. 认证层:JWT 签名/过期(中间件);
  2. 接口层:ApiAuthorizeFilter(全局)+ [ApiActionPermission(ActionPermissionOptions.XXX)](Action 级)→ 查 Sys_Menu.Auth 中该表的按钮权限值;[PermissionTable(Name=...)] 绑定表名;
  3. 数据层:字段级权限 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 文件) ​

过滤器类型职责
ApiAuthorizeFilterIAuthorizationFilter(全局)JWT 身份补全 + vol_exp 头 + 匿名白名单
ActionPermissionFilterIAsyncActionFilter[ApiActionPermission] 按钮权限判定
ActionExecuteFilterIActionFilter(全局)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 权限,普通角色需手动授权。
  • 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.csAI 生成 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消息推送开关
SecretJWT 签发 + DES 密钥组⚠️ 硬编码入仓库
GmCrypto国密 SM2/3/4KeyHex 空=未启用
ExpMinutesJWT 有效期(分钟),默认 120
CreateMember / ModifyMember审计字段自动填充开关
GlobalFilter演示环境只读黑名单Enable=false
UseSnowbigint 雪花 ID0
QuartzAccessKey定时任务远程回调密钥⚠️ 硬编码
LogicDelField逻辑删除字段(空=物理删除)
TenancyField / UseDynamicShareDB / DBPath / DBBackPath租户隔离/动态分库
UserAuth只看自己创建的数据0
FileAuth/Upload 静态文件授权0
RateLimiting按 IP+路径限流Enabled=false
GenControllerToDir / montior / DbTable生成器/监控/表维护开关
CorsUrls跨域白名单⚠️ 当前 CORS 策略未引用,实际 SetIsOriginAllowed(_=>true) 全放行

13. 安全与部署注意(上线前必查) ​

  1. CORS 全放行(Program.cs:130-139):任意来源+凭证;生产应收紧为白名单。
  2. Swagger 常开(挂根路径,/ 302 → /swagger/);生成方法虽被 ApiExplorerSettings 隐藏,但自定义接口全量暴露,需手动关闭。
  3. 明文连接串/JWT 密钥/Quartz 密钥入仓库(§12 ⚠️ 项),应迁移环境变量或密钥管理。
  4. 静态上传目录默认无授权(FileAuth=0),Upload 可匿名枚举访问。
  5. 菜单/权限缓存不感知直接改库(§7),运维 SQL 改菜单后必须刷版本或重启。
  6. 限流默认关闭(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
不写代码暴露表 CRUDGeneric 通用服务(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 基于源码全量探查整理;示意图章节同日补绘。