外观
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. 新增一个业务表单的标准流程
- 建表 → 前端代码生成器(
/builder/coderV3)配置表/字段/查询/明细 → 生成 实体 + Service + Controller + Vue 页面 + 菜单 - 业务逻辑全部写
Services/{模块}/Partial/{表}Service.cs(新建 partial 类文件,重写虚方法/委托) - 自定义接口写
Controllers/{模块}/Partial/{表}Controller.cs - 配菜单权限(系统管理→菜单配置)→ 角色授权。⚠ 菜单
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 化(默认内存,单机可用)