Skip to content

VOL Pro 后端开发指南 ​

适用:api_sqlsugar/(.NET 8 + SqlSugar/EF 双引擎)。 所有写法均经源码验证;标注 ⚠ 的是实战踩过的坑。官方文档站 http://doc.volcore.xyz(页面需加 .html 后缀)。


1. 项目分层与改动边界 ​

项目内容改不改
VolPro.Core框架内核:BaseProvider(ServiceBase/Repository)、WorkFlow、FormWorkFlow、Quartz、SignalR、Filters、Tenancy尽量不改,影响全局
VolPro.Entity实体。DomainModels/ 下按模块分目录生成器产出的主实体文件不改;扩展字段写独立 partial:DomainModels/erp/ErpBillExt.cs
VolPro.Sys系统模块(用户/菜单/角色/字典/工作流)业务扩展写 Services/**/Partial/、Controllers/**/Partial/
VolPro.ERP/Mes/AI/DbTest业务模块(ERP 42 个实体是官方演示级)随便改, ours
VolPro.WebApi启动项目 + Controllers自定义接口写 Controllers/**/Partial/

判别口诀:内核(Core)的改动要想三遍;业务(ERP/Mes)的改动放开手;生成器产物(实体主文件、Service 骨架)只通过 Partial 扩展。

2. 新增一个业务表单的标准流程 ​

  1. 建表 → 前端代码生成器(/builder/coderV3)配置表/字段/查询/明细 → 生成 实体 + Service + Controller + Vue 页面 + 菜单
  2. 业务逻辑全部写 Services/{模块}/Partial/{表}Service.cs(新建 partial 类文件,重写虚方法/委托)
  3. 自定义接口写 Controllers/{模块}/Partial/{表}Controller.cs
  4. 配菜单权限(系统管理→菜单配置)→ 角色授权。⚠ 菜单 Auth 是"可勾选项全集",普通角色的实际权限存在 Sys_RoleAuth(UserAuthArr);admin(Role_Id=1)是超级管理员,直接取菜单 Auth 全集

⚠ 菜单/权限缓存坑:Sys_MenuService 用进程内 static _menus + 内存版本号缓存。直接改库(Sys_Menu.Auth、角色授权)不生效,必须:后台界面保存一次菜单,或重启后端。改完重新登录(前端权限是登录时拉的)。

3. ServiceBase 钩子体系(核心,必背) ​

主从表保存/更新/删除/审核全部走 ServiceBase<T> 的委托钩子。写在 Partial Service 里,末尾必须 return base.Xxx(...),钩子里 return WebResponseContent.Error(...) 或抛异常即整体回滚(base.Add 内部已开事务)。

csharp
public partial class ERP_PurchaseOrderService   // 生成器骨架同款声明
{
    // ---------- 新增 ----------
    public override WebResponseContent Add(SaveModel saveDataModel)
    {
        AddOnExecuting = (ERP_PurchaseOrder order, object list) =>
        {
            var details = list as List<ERP_PurchaseOrderDetail>;  // 明细集合
            if (order.OrderDate > DateTime.Now) return WebResponseContent.Error("日期不能是未来");
            return WebResponseContent.OK();
        };
        AddOnExecuted = (order, list) => { /* 落库后:写库存/应收等副作用 */ return WebResponseContent.OK(); };
        return base.Add(saveDataModel);
    }

    // ---------- 更新:UpdateOnExecuting / UpdateOnExecuted,签名同上 ----------
    // ---------- 删除:DelOnExecuting = (object[] keys) => {...};删除前校验下游单据 ----------

    // ---------- 审核 ----------
    public override WebResponseContent Audit(object[] keys, int? auditStatus, string auditReason)
    {
        AuditWorkFlowExecuting = (o, status, lastAudit) => { return WebResponseContent.OK(); };
        AuditWorkFlowExecuted  = (o, status, nextUserIds, lastAudit) =>
        {
            if (lastAudit == true) { /* 流程终点:扣库存、写应收 */ }
            return WebResponseContent.OK();
        };
        return base.Audit(keys, auditStatus, auditReason);   // 回退:base.RewriteFlow(order)
    }
}

要点:

  • SaveModel:MainData(字典)/ DetailData(明细列表)/ DelKeys / Details(多子表)/ IsFlow(走审批)/ DataVersionField(乐观锁)。定义在 VolPro.Entity/DomainModels/Core/SaveDataModel.cs
  • 明细校验放 AddOnExecuting(此时已转实体集合);跨表副作用放 AddOnExecuted/AuditWorkFlowExecuted
  • 异步版:AddAsync + AddOnExecutingAsync(生成器勾"异步接口"才生成)

📄 文档:/docs/cs/service/add.html、update、del、audit、antiAudit(反审)、flow

4. 查询与合计 ​

csharp
public override PageGridData<T> GetPageData(PageDataOptions options)
{
    // 固定过滤:只看未删除的
    QueryRelativeExpression = q => q.Where(x => x.Status != 9);
    // 后端合计
    SummaryExpress = (ISugarQueryable<T> q) => q.Select(x => new {
        Qty  = SqlFunc.AggregateSum(x.Qty),
        Amount = SqlFunc.AggregateSum(x.Amount)
    }).FirstOrDefault();
    return base.GetPageData(options);
}
// 明细合计:protected override object GetDetailSummary<Detail>(ISugarQueryable<Detail> q)
  • 查询契约 PageDataOptions:Sort/Order、Filter(自定义查询,支持 or/多字段/分组)、Columns、Summary、Export
  • ⚠ SQL 三值逻辑(实战踩过):可空字段(如 int? Status)库里存着 NULL 时,x.Status != 4 在 SQL 层求值 UNKNOWN → 整行被过滤,"选单永远查不到数据"。必须写 (x.Status == null || (x.Status != 4 && x.Status != 5))。改过滤前先 SELECT 状态列, COUNT(*) GROUP BY 看看 NULL 占比。注意:内存里(ToList 之后)null != 4 是 true,不受影响——只有 SQL 层要防

📄 文档:/docs/cs/service/search.html、detail.html、/docs/cs/dev/db/join.html(主从明细关联查询)

5. 事务与库存类副作用 ​

  • 单表/主从表:base.Add/Update 内部已开事务,钩子返回 Error 即回滚,不要自己再包
  • 跨表副作用(写库存/应收/反写上游):写在与调用方同一 DbContext 里自然同事务。手写服务用 new 直接传 Repository(与官方示例 ErpInventoryStockService 一致):
csharp
// 手写服务:不走 DI 注册,与主流程共享 DbContext/事务
public class ErpPurchaseInboundHelper
{
    private readonly IRepository<ERP_PurchaseInDetail> _detailRepo;
    public ErpPurchaseInboundHelper(IRepository<ERP_PurchaseInDetail> detailRepo)
    { _detailRepo = detailRepo; }   // 调用处 new 出来传入
}
  • 需要独立事务:repository.DbContextBeginTransactionAsync(async () => { ... })
  • 原生 SQL/存储过程:/docs/cs/dev/db/EFsql.html、Dapper 系列同目录
  • 库存纪律(本项目约定):库存操作唯一入口 InventoryService.UpdateStock,严禁各单据自写 SQL 改库存;审核通过才扣减(lastAudit==true);反审/冲销不允许负库存

6. 自定义接口规范(⚠ 415 坑,实测) ​

前端 axios 全局 Content-Type: application/json。裸标量 + [FromBody] 必然 415(官方示例 voidBill 就是这么写的,别照抄)。规范写法:

csharp
// Controllers/{模块}/Partial/ERP_PurchaseInController.cs
public partial class ERP_PurchaseInController
{
    [HttpPost, Route("mergeOrderDetail")]
    [ApiActionPermission(ActionPermissionOptions.Edit)]   // 按钮级接口权限
    public async Task<IActionResult> MergeOrderDetail([FromBody] MergeOrderInput input)
    {
        var result = await _service.MergeOrderDetailAsync(input);
        return Json(result);
    }
}
// Service Partial 里实现业务

// DTO:每个属性显式指定命名策略(框架全局命名策略不确定,不写可能静默绑成 null)
public class MergeOrderInput
{
    [JsonPropertyName("supplierId")]  public Guid SupplierId { get; set; }
    [JsonPropertyName("detailIds")]   public List<long> DetailIds { get; set; }
}

⚠ 不写 [JsonPropertyName] 的典型症状:Guid.Empty / 空 List,“静默查不到数据”不报错——比 415 更难查。 🔧 排查利器:/swagger/v1/swagger.json 直接看 requestBody.$ref 的属性名,无需登录。

7. 依赖注册规则(⚠ 顺序错 = 服务 404) ​

  • 框架按 GetInterfaces()[0] 注册 IDependency 实现类 → 手写 Repository 的声明必须是生成器模板顺序: class XxxRepository : RepositoryBase<T>, IXxxRepository(接口放最后)
  • Service 同理:class XxxService : ServiceBase<T>, IXxxService(partial 骨架已写好,别调整继承列表顺序)
  • 拿当前用户:UserContext.Current.UserId / RoleIds / IsRoleIdSuperAdmin(roleIds)(Role_Id=1 即超管)

8. 导入导出与文件上传 ​

csharp
public override WebResponseContent Export(PageDataOptions pageData)
{
    pageData.Limit = 5000;                                   // 最大导出行数
    pageData.ExportColumns = x => new { x.ReceiptNo, x.Amount };  // 导出列
    pageData.ExcelHeaderMap = new Dictionary<string, string> { ["ReceiptNo"]="入库单号" };
    ExportCustomValue = (value, col, entity, index) => value;     // 自定义单元格值
    return base.Export(pageData);
}
// 导入:DownLoadTemplate/Import 已内置(EPPlus、事务写入、支持明细导入)
// 上传:只写本地磁盘,改相对路径:UploadFolder = $"erp/{DateTime.Now:yyyyMMdd}";

⚠ 框架无 OSS/云存储(uploadOSS 是空实现,文档无云存储章节)。要云存储需自建,建议 MinIO:后端加 STS 凭证接口 + 前端 VolUpload.vue 的 uploadOSS() 填实,约 1–2 人日。

📄 文档:/docs/cs/service/exportData.html、importData.html、upload.html

9. 审核流选型与定时任务 ​

  • FormWorkFlow(表单审批流,VolPro.Core/FormWorkFlow/):ERP 单据审批用这个,界面配置节点
  • WorkFlow(图状流程,VolPro.Core/WorkFlow/):复杂条件分支才用
  • 接入:单据表加审批字段 → 保存时 IsFlow=true → 审核 Audit(keys, status, reason) 自动推进;节点到达自动写待办 + SignalR 推送(FormWorkFlowNotify)

定时任务(Quartz):界面增删改启停 + 日志(Sys_QuartzOptions/Sys_QuartzLog);类型支持 HTTP 回调/本地方法/脚本。 ⚠ 内存调度器,无持久化 JobStore——多实例部署会重复执行。上负载均衡前必须补 ADO JobStore 或任务单实例化。

10. 常用工具速查 ​

需求写法文档
单据编码实体主键配编码规则,保存时自动 CreateCode();自定义:/docs/cs/dev/code.html生成单据号/流水
字典DictionaryManager.GetDictionary("audit")/docs/cs/dev/dic.html
缓存CacheContext.Get/Add(UseRedis=false 时为内存;切 Redis 改 appsettings)/docs/cs/dev/cache.html
多库SqlServer/MySql/Pg/Oracle/达梦/金仓,连接串 Connection 节点/docs/cs/dev/db.html
租户/分库TenancyId、UseDynamicShareDB/docs/cs/dev/tenancy.html
日志Logger.Info/Error( SeriLog,落 Log/ 目录)/docs/cs/dev/log.html
当前用户/权限UserContext.Current;接口权限 [ApiActionPermission(ActionPermissionOptions.Xxx)]/docs/cs/dev/userauth.html

11. 上线前检查清单 ​

  • [ ] Quartz 多实例问题已决策(单实例 or 补 JobStore)
  • [ ] OSS 已替换本地存储
  • [ ] 可空状态列的查询全部防御 NULL
  • [ ] 所有自定义接口 DTO 显式 JsonPropertyName
  • [ ] 库存/金额操作收敛到唯一服务入口,金额精度与税率规则建模前定死
  • [ ] SignalR/缓存确认是否需要 Redis 化(默认内存,单机可用)